Skip to content

feat(program-escrow): fee-on-transfer audit with regression tests (closes #946)#991

Merged
Jagadeeshftw merged 5 commits intoJagadeeshftw:masterfrom
precious-akpan:feature/program-escrow-fee-on-transfer-audit
Apr 1, 2026
Merged

feat(program-escrow): fee-on-transfer audit with regression tests (closes #946)#991
Jagadeeshftw merged 5 commits intoJagadeeshftw:masterfrom
precious-akpan:feature/program-escrow-fee-on-transfer-audit

Conversation

@precious-akpan
Copy link
Copy Markdown
Contributor

Summary

Resolves #946 — Fee-on-transfer (FoT) token handling review for program-escrow.

This PR audits and validates program-escrow accounting under deflationary / fee-on-transfer token conditions. All existing tests continue to pass (78 passing, 0 failing).


Changes

contracts/program-escrow/src/test_fee_on_transfer.rs [NEW]

A purpose-built DeflatToken mock contract charges a configurable basis-point fee on every transfer / transfer_from call, faithfully simulating real-world FoT / deflationary tokens. 12 edge-case integration tests:

# Test What it validates
1 test_fot_lock_10pct_fee_credits_actual_received 10 % FoT fee on inbound lock — remaining_balance reflects actual_received, not requested amount
2 test_fot_lock_50pct_fee_credits_actual_received 50 % high-fee scenario
3 test_fot_repeated_locks_accumulate_actual_received 3× sequential locks accumulate correctly
4 test_fot_lock_v2_rejects_amount_exceeding_actual_balance lock_program_funds_v2 detects FoT mismatch and panics
5 test_fot_lock_v2_accepts_actual_received_amount lock_program_funds_v2 succeeds when amount == actual balance
6 test_zero_fee_token_behaves_like_standard_sac Zero-fee token is identical to standard SAC token
7 test_fot_remaining_balance_never_exceeds_on_chain_balance Invariant: accounting ≤ on-chain reality
8 test_fot_lock_then_payout_succeeds_within_actual_balance Payout from FoT-funded pool succeeds
9 test_fot_lock_then_over_payout_panics Over-payout panics — cannot draw more than credited balance
10 test_fot_funds_locked_event_reflects_actual_received FundsLocked event carries actual_received, not requested amount
11 test_fot_cumulative_balance_invariant_across_sequential_locks Multi-depositor sequential locks — invariant holds
12 test_fot_batch_payout_distributes_correctly_from_credited_funds Batch payout from FoT-funded escrow

contracts/program-escrow/src/lib.rs

  • Registered mod test_fee_on_transfer in the test module block.
  • Fixed gas_optimization::efficient_math::safe_sub_zero — was using checked_sub which only returns None on arithmetic overflow (not on negative results); replaced with explicit if b > a { 0 } else { a - b }.
  • Fixed test_draft_state::test_operations_succeed_after_publish — added token.approve() before lock_program_funds_from (which uses transfer_from, requiring a pre-approved allowance).

Security Notes

  • Inbound (lock): lock_program_funds_from measures balance_before → balance_after and credits only actual_received. FoT fees are transparently reconciled.
  • Inbound (v2 lock): lock_program_funds_v2 asserts contract_balance ≥ requested_amount before crediting, catching any FoT shortfall.
  • Outbound (payout): The escrow debits remaining_balance by the requested release amount and calls token::transfer. If the token silently burns a fee on the outbound transfer, the recipient receives less than requested. This is documented but cannot be prevented at the escrow layer — it is inherent to non-standard FoT tokens.
  • Recommendation: Use standard Stellar SAC tokens only. Non-standard FoT tokens are not a supported use case.

Test Results

running 84 tests
test result: ok. 78 passed; 0 failed; 6 ignored

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

@precious-akpan is attempting to deploy a commit to the Jagadeesh B's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 31, 2026

@precious-akpan 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

@precious-akpan precious-akpan force-pushed the feature/program-escrow-fee-on-transfer-audit branch from ba9e383 to a0c8161 Compare March 31, 2026 20:22
i128::checked_sub only returns None on arithmetic overflow (e.g.,
i128::MIN - 1), not when the result is negative. safe_sub_zero(5, 10)
was therefore returning -5 instead of 0.

Replace with an explicit guard: if b > a { 0 } else { a - b }.

Fixes test: gas_optimization::tests::test_safe_sub_zero
…from

lock_program_funds_from uses token::transfer_from internally, which
requires the depositor to have granted an allowance to the contract
before the call. The test was minting tokens to the depositor but
not calling token.approve(), causing an 'not enough allowance' failure
at the token level.

Add: token.approve(&depositor, &client.address, &5000, &99999)

Fixes test: test_draft_state::test_operations_succeed_after_publish
…gadeeshftw#946)

Implements issue Jagadeeshftw#946 requirement: simulate deflationary / inconsistent
token transfer behaviour using a purpose-built DeflatToken mock contract
that charges a configurable basis-point fee on every transfer() and
transfer_from() call.

Adds test_fee_on_transfer.rs with 12 integration test scenarios:

  1. 10 % inbound fee — actual_received credited, not requested amount
  2. 50 % high-fee inbound lock scenario
  3. Repeated sequential FoT locks — cumulative balance invariant
  4. lock_program_funds_v2 rejects amount > actual on-chain balance
     (FoT mismatch detection guard)
  5. lock_program_funds_v2 accepts amount == actual received after fee
  6. Zero-fee token is identical to standard SAC token behaviour
  7. remaining_balance never exceeds actual on-chain balance (invariant)
  8. FoT lock then payout within credited funds succeeds
  9. Over-payout attempt panics (cannot draw more than credited balance)
 10. FundsLocked event carries actual_received, not requested amount
 11. Multi-depositor sequential FoT locks — cumulative invariant holds
 12. Batch payout from FoT-funded escrow — accounting correct

Security finding documented in tests: outbound payout transfers via
token::transfer ALSO incur FoT fees; recipients receive net-of-fee
tokens while escrow debits remaining_balance by the full release amount.
This is the expected contract behaviour (cannot be prevented at the
escrow layer) and is now explicitly documented in test comments.

Also registers the new module in lib.rs (mod test_fee_on_transfer).

Test suite: 78 passed, 0 failed.
@precious-akpan precious-akpan force-pushed the feature/program-escrow-fee-on-transfer-audit branch from a0c8161 to 1888ac9 Compare March 31, 2026 21:03
@Jagadeeshftw Jagadeeshftw merged commit 6ca8d27 into Jagadeeshftw:master Apr 1, 2026
7 of 9 checks passed
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.

Smart contract: Fee-on-transfer token handling review — program-escrow

2 participants