Skip to content

Constrained, admin-configured protocol fee recipients - #1092

Merged
IbrahimIjai merged 11 commits into
SO4-Markets:mainfrom
toluwalasedaniel459-cmyk:feat/constrained-fee-recipients
Oct 21, 2026
Merged

IbrahimIjai merged 11 commits into
SO4-Markets:mainfrom
toluwalasedaniel459-cmyk:feat/constrained-fee-recipients

Conversation

@toluwalasedaniel459-cmyk

@toluwalasedaniel459-cmyk toluwalasedaniel459-cmyk commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Closes #969

⚠️ Merging this PR restores the codebase #1090 deleted. #1090 (280f268) removed 765 files from main, and #1083, #1089 and #1084 were merged on top of that tree. To make this PR mergeable, main was merged into this branch, keeping the full tree. This PR therefore re-adds every file #1090 deleted (about 1.17M lines, identical to main at 5436392) and keeps the 6 files main added since:
libs/clob_settlement_core/src/lib.rs, contracts/reader/src/clob_reads.rs, tests/reentrancy_token.rs, docs/REENTRANCY_ANALYSIS.md, docs/clob-read-api-specification.md and docs/market-data-streaming.md. None of these 6 are compiled yet: there is no Cargo.toml, mod declaration or test registration for them.

To review only this PR's own changes, diff against 5436392, the last main commit before #1090.

CI: main's .github/workflows were also deleted by #1090, so no checks run on this PR upstream. They were run on the fork instead (see CI status).

Summary

Protocol fees could be paid to any address the FEE_KEEPER passed to claim_fees / claim_all_fees. The destination is now trusted configuration only.

  • Trusted recipients in fee_handler storage: a protocol-wide recipient (set_fee_receiver) and an optional per-market override for issuer/partner payouts (set_market_fee_receiver(market, Some(r) | None)). Both require the stored admin's auth.
  • Auditable: every change emits FeeReceiverSet { market, previous, receiver } (topic fee_rcv).
  • No caller-supplied destination: claim_fees(keeper, market, token) and claim_all_fees(fee_handler, keeper, markets, tokens) no longer take receiver.
  • Fails safe: FeeReceiverNotSet (#7 — Create a config key catalog #7) when nothing is configured (fees stay claimable); InvalidFeeReceiver (#8 — Create an event catalog #8) for the market pool or the handler itself, checked at set time and at claim time.
  • Views: get_fee_receiver(market) (effective), get_protocol_fee_receiver(), get_market_fee_receiver(market).
  • v1 is single-recipient, no split economics.
  • Settlement unchanged: fees still accrue to claimable_fee_amount(market, token) alongside pool_amount; no order parameter is consulted for the fee destination. The resource benchmark confirms every existing lifecycle scenario is byte-for-byte unchanged.

⚠️ Breaking changes / upgrade

  • claim_fees and claim_all_fees signatures changed; BatchFeesClaimed dropped its receiver field. Frontend/keeper callers must update. TS bindings for fee-handler and fee-batch-sweeper are regenerated.
  • After upgrading a deployed fee_handler, the admin must call set_fee_receiver — claims revert until then. scripts/deploy.sh now sets it on fresh deploys (FEE_RECEIVER, default $ADMIN).
  • New storage: InstanceKey::ProtocolFeeReceiver, PersistentKey::MarketFeeReceiver(market). No migration of existing data needed.

Commits

Commit What
152628f Pre-existing: restore the workspace build broken by #1085/#1086 (compile fixes only)
d8485b1 The feature (contract, tests, docs, deploy script, bench scenarios)
a48ab0a Pre-existing: make clippy --all-targets -D warnings pass (#1086 order_handler tests that never compiled, #1082 clob_model lints, an oracle constant assertion)
bd16f92 Regenerated fee-handler / fee-batch-sweeper bindings
4360343 Empty commit (CI trigger on the fork)
6c127fb Pre-existing: deposit/withdrawal stale-price tests advance past oracle#1205's 20-ledger freshness window
ca1ef17 Benchmark baseline for the three new fees/* scenarios (from CI run 39335378079)
a3d265b Pre-existing: wasm-size.yml builds only deployable contracts (make build-wasm)
799c591 Pre-existing: clob_properties generator could reduce an order already at qty 1 and underflow
3053843 Merge main up to #1088 (5436392, before #1090)
4233d6b Merge current main, keeping the full tree (restores files #1090 deleted) and taking the 6 files added since

Branched from 1e6ca2f and merged with main (see the note at the top). #1087/#1088 add docs and libs/types files that are not yet declared as modules, so they don't affect the build.

The pre-existing fixes are separate commits so they can be reviewed or cherry-picked independently; none change production behaviour.

Tests

  • fee_handler unit tests: routes to the configured recipient only (keeper gets nothing); no recipient → revert with fees/pool accounting untouched; market override + clear → fallback; admin-auth positive and negative paths for both setters; invalid recipients rejected; audit events with previous/new values.
  • fee_batch_sweeper: each market in a batch pays its own configured recipient.
  • tests/fee_receiver_routing.rs (e2e): an executed order whose receiver is an attacker address still pays the settled fee to the treasury; a market override receives it instead of the treasury.
  • tests/resource_bench.rs: fees/set_fee_receiver, fees/set_market_fee_receiver, fees/claim_fees.

CI status (at 799c591, run on the fork toluwalasedaniel459-cmyk#1)

Check Result
rustfmt ✅
clippy (all targets, -D warnings) ✅
build contracts (wasm32v1-none, all 23) ✅ (also stellar contract build locally)
Auth Audit Check ✅
Resource Benchmarks ✅
cargo test ❌ — only tests/funding_rate.rs, see below
WASM Size Budget ❌ — PR build passes; the base (main) build fails because main does not compile until this PR merges

cargo test passing before it stops: every contract and lib crate, including fee_handler (34) and fee_batch_sweeper (7); and the integration tests including fee_receiver_routing (both tests), funding_conservation_e2e, clob_model, clob_properties. Because cargo test stops at the first failing binary, test files after funding_rate have not run on CI yet.

After the merge with main (4233d6b, fork CI runs 39450558514 and 39450566536): rustfmt, clippy, the WASM build and Resource Benchmarks pass again. cargo test fails only on the same 6 funding_rate tests.

⚠️ Not fixed here: tests/funding_rate.rs (6 of 7 fail) — needs a maintainer decision

These tests are in funding money-path code this PR does not touch. They had not run since main stopped compiling, and they now disagree with libs/market_utils. Per AGENTS.md I have not changed either side; someone needs to decide whether the code or the tests are wrong.

Failures: funding_rate_zero_when_no_oi (factor 3600, expected 0), funding_rate_longs_pay_shorts_when_long_oi_exceeds_short and …shorts_pay_longs… (funding per size 0), test_funding_rate_accumulation_and_settlement_integration, funding_settlement_accumulates_claimable_amount, funding_rate_zero_when_oi_balanced.

Findings:

  1. Ramp formula (possible production bug). compute_next_funding_factor caps the per-tick change at mul_div_wide(increase_factor, dt, FLOAT_PRECISION). With the tests' "100% per second" setting (increase_factor = FLOAT_PRECISION), the cap over one hour is dt = 3600 raw units (3.6e-27), so funding per size rounds to 0. GMX applies fundingIncreaseFactorPerSecond * durationInSeconds with no division. If the division is wrong, funding barely ramps on-chain either. This dates from the original market-utils commit (4b16a66). funding_conservation_e2e passes only because it sets the ramp to 1000 * FLOAT_PRECISION.
  2. Exponent units. The tests set funding_exponent_factor = 1 (raw); the passing conservation test uses FLOAT_PRECISION, which suggests the exponent's units changed and these tests weren't updated.
  3. Zero-OI expectation. funding_rate_zero_when_no_oi expects a zero factor right after a full close, but funding state is updated at the start of the closing order, while OI is still non-zero, so a non-zero factor legitimately remains until the next update.

Suggest tracking (1) as its own issue; (2) and (3) look like stale tests.

Not covered by CI

  • make deploy-all / make testnet-smoke (deploy path changed in scripts/deploy.sh) have not been run.
  • Bindings for 11 other contracts are stale only because of feat: typed error model, market lifecycle states, and versioned event schema #1085's gmx-types additions (event/state/error types now in every spec); left untouched. The CLI's @stellar/stellar-sdk ^14 → ^16 package.json bump was intentionally not included.

🤖 Generated with Claude Code



main did not compile, so no gate step could run:
- tests/Cargo.toml: two [[test]] headers were missing (duplicate keys)
- settlement_invariants/rollback_harness: depended on nonexistent
  `gmx_types`/`gmx_math` package names; fixed unused-arg/while-let/clippy
  issues, a nonexistent `soroban_sdk::Status`, a u32/u64 mismatch, an
  invalid `Env::new()` call, and a test asserting on a nonexistent field
- gmx-types event_schema: `[u8; 32]` is not a contracttype; use BytesN<32>
- order_cleanup: u32/usize comparison in the batch limit check
- cargo fmt on the touched modules

Compile fixes only; no behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Protocol fees could be paid to any address the FEE_KEEPER passed to
claim_fees / claim_all_fees. The destination is now trusted state only.

- Protocol recipient (instance storage) set by `set_fee_receiver`, and an
  optional per-market override (persistent storage) set/cleared by
  `set_market_fee_receiver`; both stored-admin only.
- Every change emits `FeeReceiverSet { market, previous, receiver }`
  (topic `fee_rcv`) so the full history is auditable.
- BREAKING: `claim_fees(keeper, market, token)` and
  `claim_all_fees(fee_handler, keeper, markets, tokens)` drop the
  `receiver` argument; `BatchFeesClaimed` drops its `receiver` field.
- Reverts with FeeReceiverNotSet (SO4-Markets#7) when nothing is configured, and
  InvalidFeeReceiver (SO4-Markets#8) for the market pool or the handler itself.
- New views: get_fee_receiver, get_protocol_fee_receiver,
  get_market_fee_receiver. v1 is single-recipient; no splits.
- Accrual paths are unchanged: settlement credits
  claimable_fee_amount(market, token); no order parameter is consulted.

Tests: unit tests for routing, fallback, admin auth (positive/negative),
invalid recipients, events; sweeper per-market routing; e2e test showing
an order's `receiver` cannot redirect settled fees; fee scenarios added
to the resource bench. Docs: security.md, deployment.md, auth-audit.md;
deploy.sh sets the protocol recipient after initialize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pre-existing test code that never compiled or that current stable clippy
(what CI installs) rejects; no production behaviour changes:
- order_handler tests from SO4-Markets#1086 (rollback_atomicity, settlement_invariants):
  add the missing settlement_invariants/rollback_harness dev-dependencies,
  drop an extra `initialize` argument, fix a u32/u64 comparison and a
  use-after-move, remove unused imports, rustfmt
- tests/clob_model.rs (SO4-Markets#1082): type alias for a complex return type,
  needless borrows, `div_ceil`
- oracle: turn an assertion on a constant into a compile-time check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up the new fee-recipient entrypoints, the `receiver`-less
claim_fees / claim_all_fees signatures, and errors SO4-Markets#7/SO4-Markets#8.

Generated with stellar-cli 28.1.0. The package.json bump to
@stellar/stellar-sdk ^16 that this CLI version emits was left out on
purpose (major frontend upgrade, out of scope); other contracts' bindings
are stale only because of SO4-Markets#1085's gmx-types additions and are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
execute_{deposit,withdrawal}_reverts_on_stale_price advanced the ledger by
one and expected a panic, but oracle#1205 (53abcfb) deliberately accepts
prices for PRICE_FRESHNESS_WINDOW (20) ledgers, so the calls succeed and
the #[should_panic] tests fail. Step past the window so the tests check
what they describe again. Test-only change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds fees/set_fee_receiver, fees/set_market_fee_receiver and
fees/claim_fees. Every existing scenario is unchanged versus the previous
baseline. Recorded from CI run 39335378079 (ubuntu, soroban-sdk 25.3.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cargo build --target wasm32v1-none --release` with no -p list builds every
workspace member, including test-only crates that enable soroban-sdk
`testutils`, which does not compile for wasm. Use `make build-wasm` (the
$(CONTRACTS) list), as ci.yml already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The FIFO property test picks resting orders at random to cancel or reduce,
and reduces to rng.range(1, qty - 1). Once an order has been reduced to 1,
choosing it again calls range(1, 0) and underflows ("attempt to subtract
with overflow"). Cancel it instead in that case. Generator bug in the test
only; this test never ran before because tests/Cargo.toml was missing its
[[test]] header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@toluwalasedaniel459-cmyk

Copy link
Copy Markdown
Contributor Author

Heads-up for maintainers: the conflicts on this PR are not from this branch. main was effectively wiped by #1090.

#1090 (merge commit 280f268, feature commit 31924e5) changed 765 files: +164 / −1,167,667. After it, main contains a single file, libs/clob_settlement_core/src/lib.rs. Every contract, lib, test, doc, workflow and Cargo.lock is gone, so every file this PR touches now shows as a modify/delete conflict.

You can check it with:

git ls-tree -r --name-only 280f268          # -> libs/clob_settlement_core/src/lib.rs
git show --stat 31924e5 | tail -1           # -> 765 files changed, 164 insertions(+), 1167667 deletions(-)

This branch merged cleanly with main at 5436392 (#1088), the state just before #1090. Suggested fix: revert #1090 (git revert -m 1 280f268) and re-land its CLOB settlement changes on top of the real tree. After the revert, this PR should merge without changes.

I haven't resolved the conflicts here, because accepting main's side would delete the codebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Markets#1090 deleted

SO4-Markets#1090 (280f268) deleted 765 files from main, leaving only
libs/clob_settlement_core. This merge keeps this branch's full tree, which
restores every file SO4-Markets#1090 deleted, and takes the files main added since
5436392 (SO4-Markets#1090, SO4-Markets#1083, SO4-Markets#1089, SO4-Markets#1084):

- libs/clob_settlement_core/src/lib.rs
- contracts/reader/src/clob_reads.rs
- docs/REENTRANCY_ANALYSIS.md, docs/clob-read-api-specification.md,
  docs/market-data-streaming.md
- tests/reentrancy_token.rs

None of these are compiled yet: clob_settlement_core has no Cargo.toml,
clob_reads.rs is not declared as a module, and reentrancy_token.rs is not
registered in tests/Cargo.toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Oct 20, 2026

Copy link
Copy Markdown

@toluwalasedaniel459-cmyk 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.

[CLOB-093] Implement optional market-specific fee recipient configuration

2 participants