feat(ledger): book crypto-native fees natively (exchange-tx, trading-order, payout-order) (#4277)#4279
Open
TaprootFreak wants to merge 2 commits into
Open
feat(ledger): book crypto-native fees natively (exchange-tx, trading-order, payout-order) (#4277)#4279TaprootFreak wants to merge 2 commits into
TaprootFreak wants to merge 2 commits into
Conversation
…4277) Draft for evaluation — the honest recommendation is hold/won't-fix given an immaterial benefit against a high semantic blast radius (see the PR description). The ccxt Trade branch books the venue fee as a CHF-only spread leg valued at the pre-2dp-rounded feeAmountChf, so a sub-cent fee (a Binance micro-fill's BNB commission) rounds to 0 and no leg is booked. And because the quote-spread plug neutralizes it, the fee has zero net P&L effect on every ccxt fill and its native outflow is never tracked. Mirror payout-order appendDistinctFeeLegs: book the fee against its native ASSET/{exchange}/{feeCurrency} account (mark-valued) so the native outflow survives even when its CHF value rounds to 0. Conservative: only for a distinct fee asset (not base/quote) with a bootstrapped account and a historical mark, else fall back to today's CHF spread leg — no trade that books today defers.
#4277) Extends native-fee booking to the two sibling sites found by the #4277 audit, matching the exchange-tx appendCcxtFeeLeg template: - trading-order: the on-chain arbitrage gas (txFeeAmount, native) is booked against its chain's native COIN asset (assetService.getNativeAsset(assetIn.blockchain) - the exact asset the persisted txFeeAmountChf was valued from), Cr ASSET/{coin} alongside the unchanged CHF EXPENSE/network-fee Dr, so a sub-cent gas fee's native outflow survives. Conservative: distinct coin only, non-throwing account, mark-gated -> no new deferral. Injects AssetService. - payout-order: appendDistinctFeeLegs no longer early-returns when the persisted CHF sum is 0 - it gates only the CHF EXPENSE leg on feeChf and still builds the per-distinct-asset native legs, so a sub-cent distinct fee keeps its native outflow. Non-throwing / mark-gated at feeChf===0 -> no new deferral or fail-stop.
Collaborator
Author
|
Reviewed across two dimensions (conformance/quality + logic/correctness), each finding adversarially verified — zero confirmed findings.
CI green (Build and test, review, CodeQL, analyze). Verified on Node 20: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #4277 and its two sibling sites. Supersedes #4278 (exchange-tx-only draft, folded in here).
The anti-pattern
A crypto-native fee (a real BNB commission / on-chain gas) is booked into the ledger only via a persisted, 2-dp-rounded
*AmountChffield. When that CHF value is sub-cent it rounds to0.00and the native outflow is lost — no leg is booked. The ledger books in integer CHF cents, so a sub-cent fee cannot survive as CHF; it can only be preserved by booking the underlying native amount (× its mark), exactly aspayout-order.consumer.appendDistinctFeeLegsalready does for distinct fee assets.A repo-wide audit found three sites (fiat/CHF-denominated 2-dp values are correct and were not touched). All three are fixed here with the same conservative template — native leg only for a distinct fee asset with a bootstrapped account and a historical mark, else fall back to today's CHF behaviour, so no tx that books today starts deferring.
The three fixes
exchange-tx.consumer— ccxt venue fee. NewappendCcxtFeeLegbooks the fee againstASSET/{exchange}/{feeCurrency}(mark-valued). Deeper finding: the quote-spread plug (residualChf = -Σ amountChf) makestotal spread-{exchange} = quoteChf − baseChfindependent offeeAmountChf, so today the venue fee is algebraically neutralized on every ccxt fill (not just sub-cent ones) — neither recognized as a cost nor tracked natively (§7 reconciliation only observes the resulting BNB drift, so nothing is silently corrupted). This fix recognizes the fee asspread-{exchange}expense on all ccxt trades and starts native fee-asset tracking → the highest-reach of the three. Scrypt's market-spread branch is untouched.trading-order.consumer— arbitrage gas. NewappendNetworkFeeLegsbooks the on-chain gas (txFeeAmount, native) against the swap chain's native COIN —assetService.getNativeAsset(assetIn.blockchain), the exact asset the persistedtxFeeAmountChfwas valued from — as aCr ASSET/{coin}alongside the unchanged CHFEXPENSE/network-feeDr; the arbitrage plug simply shrinks by the gas CHF (no double-count). InjectsAssetService(SharedModule; DI verified). Low blast radius.payout-order.consumer— distinct-fee gate.appendDistinctFeeLegsno longer early-returns when the persisted CHF sum is0; it gates only the CHFEXPENSE/network-feeleg onfeeChfand still builds the per-distinct-asset native legs, so a sub-cent distinct fee keeps its native outflow. Non-throwing + mark-gated atfeeChf===0→ no new deferral/fail-stop. Lowest blast radius (the native machinery already existed and was tested on thefeeChf!==0path). The payout-asset fee fold and thefeeChf!==0path are unchanged.No new account names, no CoA/bootstrap change, no migration; forward-only. Sign-aware off the signed native amount (a rebate flips Cr→Dr).
Materiality & blast radius
The recovered amounts are immaterial (~tens of CHF over ~20 months for exchange-tx; unquantified but plausibly larger for trading-order L1 gas; narrowest for payout-order). The exchange-tx change has real semantic reach — it re-prices the venue fee as recognized expense on all ccxt trades, starts native BNB tracking, nudges the §7 Binance/BNB + equity-parity baselines toward correct (§7 is a pure observer → no double-correction), and triggers a bounded reverse+rebook of the recently-synced trade window on first deploy. trading-order/payout-order are low-reach. Merge decision (accepting the exchange-tx reach) stays with the maintainer.
Verification (Node 20)
prettier/eslint/type-check/buildclean · DI graph resolves (incl. the newAssetServicedependency) · full accounting suite 787/787 with 10 new tests:feeChf===0; conservative skip for an unvaluable distinct fee asset atfeeChf===0.The existing fee tests now also exercise the CHF fallback / unchanged paths as regression guards (e.g. the
feeChf!==0distinct-fee-asset defer test stays green).