Skip to content

feat: add immutable protocol fee split on SME withdrawal with tests a…#590

Open
TYDev01 wants to merge 1 commit into
Liquifact:mainfrom
TYDev01:feature/contracts-10-protocol-fee
Open

feat: add immutable protocol fee split on SME withdrawal with tests a…#590
TYDev01 wants to merge 1 commit into
Liquifact:mainfrom
TYDev01:feature/contracts-10-protocol-fee

Conversation

@TYDev01

@TYDev01 TYDev01 commented Jun 29, 2026

Copy link
Copy Markdown

close #316

Support a configurable protocol fee deducted at SME withdrawal (#316)

Implements an immutable protocol_fee_bps configured at init that, on
withdraw, splits funded_amount into an SME payout and a treasury fee. Closes #316.

What changed

escrow/src/lib.rs

  • init — new final param protocol_fee_bps: Option<i64>, validated 0..=10_000
    (ProtocolFeeBpsOutOfRange), stored under the new additive DataKey::ProtocolFeeBps
    (default 0; always written so reads never branch on absence).
  • withdraw — checked split:
    fee        = funded_amount * protocol_fee_bps / 10_000   (integer floor, checked)
    sme_payout = funded_amount - fee                          (checked)
    
    feeDataKey::Treasury, sme_payoutsme_address, both via the existing
    SEP-41 balance-delta–checked transfer wrapper. The treasury transfer is skipped when
    fee == 0 (one transfer, identical to the pre-fee path); the SME transfer is skipped
    when net == 0 (only at 10_000 bps). DistributedPrincipal advances by the full
    gross funded_amount.
  • SmeWithdrew — append-only fee field; amount is now the net SME payout.
  • New typed errors WithdrawFeeArithmeticOverflow (181), WithdrawNetArithmeticUnderflow
    (182), ProtocolFeeBpsOutOfRange (180); new view get_protocol_fee_bps.

Tests (escrow/src/tests/integration.rs) — fee split & treasury delta, zero-fee
default, max-bps (100% → treasury), floor-rounding residue to SME, tiny-bps floor-to-zero,
large overflow-safe amount, getter, event payload, init range rejection.

Docsescrow-numeric-model.md, README.md, ESCROW_SME_WITHDRAWAL.MD (on-chain
disbursement interaction), escrow-error-messages.md, EVENT_SCHEMA.md.

Security notes

  • Conservation: sme_payout + fee == funded_amount for every withdrawal — no principal
    created or destroyed.
  • Floor rounding toward the SME — the treasury is never over-credited by rounding.
  • Overflow safety: funded_amount is not bounded by MAX_INVOICE_AMOUNT (over-funding
    is allowed), so the fee multiply/divide and the net subtract use checked arithmetic with
    typed errors rather than silent wrap.
  • Bounds & immutability: fee validated 0..=10_000 at init; never mutated after.
  • Backward compatible: protocol_fee_bps == 0 (or omitted) reproduces the exact pre-fee
    behavior; DataKey::ProtocolFeeBps is additive (reads 0 on older instances).
  • Dependency on on-chain disbursement: the fee is realized only on the on-chain withdraw
    path, not off-chain settle, refund, or claim_investor_payout — documented in
    ESCROW_SME_WITHDRAWAL.MD.

⚠️ Build/test status — please read

The base branch does not compile due to pre-existing errors unrelated to #316
(undeclared PausedBlocks* variants, duplicate guard_status_* fns, a duplicate 201
discriminant, a tier_lock_secs double-assign, and a duplicate import in integration.rs).
This PR intentionally does not touch them, so cargo build / cargo test still fail on
that pre-existing breakage.

The protocol-fee logic was verified by temporarily stubbing those pre-existing errors and
running the new tests through a throwaway external test target (which compiles only the
library, not the broken #[cfg(test)] modules); the stubs were then fully reverted. Result:

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

With the pre-existing errors stubbed, rustfmt --check is clean on all changed code.

Out of scope / follow-up

Adding the new init argument breaks the ~420 init call sites in the other test
files (coverage.rs, funding.rs, etc.). They are not updated here, since the suite
cannot compile until the pre-existing breakage above is fixed regardless. Recommended
follow-up (separate PR): fix the pre-existing compile errors, then append the trailing
&None fee arg across the remaining test files so the full suite builds and the 95% coverage
gate can be measured.

…nd docs

Add an optional, immutable protocol_fee_bps configured at init that splits
funded_amount into an SME payout and a treasury fee on withdraw.

- init: new Option<i64> protocol_fee_bps param, validated 0..=10_000
  (ProtocolFeeBpsOutOfRange), stored under additive DataKey::ProtocolFeeBps
  (default 0).
- withdraw: fee = funded_amount * fee_bps / 10_000 (floor, checked) routed to
  DataKey::Treasury; remainder to sme_address. Treasury transfer skipped when
  fee == 0 (byte-identical legacy path); SME transfer skipped when net == 0.
  DistributedPrincipal still advances by the full gross funded_amount.
- SmeWithdrew: append-only `fee` field; `amount` is now the net SME payout.
- New typed errors WithdrawFeeArithmeticOverflow / WithdrawNetArithmeticUnderflow;
  new getter get_protocol_fee_bps.
- Tests: fee split, zero-fee default, max bps, floor rounding, tiny-bps floor,
  large overflow-safe amount, getter, event payload, init range rejection.
- Docs: escrow-numeric-model, README, ESCROW_SME_WITHDRAWAL, error-messages,
  EVENT_SCHEMA.

Conservation invariant holds for every withdrawal: sme_payout + fee == funded_amount.

Refs Liquifact#316
@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@TYDev01 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.

Support a configurable protocol fee deducted at SME withdrawal

1 participant