Skip to content

feat - BZE-144 - governance-halted denoms in tradebin - #115

Draft
busydonna wants to merge 6 commits into
mainfrom
feat/BZE-144-halted-denoms
Draft

busydonna wants to merge 6 commits into
mainfrom
feat/BZE-144-halted-denoms

Conversation

@busydonna

@busydonna busydonna commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Gives BZE a generic, governance-controlled way to halt a denom on the DEX. Once a denom is listed in the new tradebin halted_denoms parameter, no new order book position and no AMM swap involving it can be opened, while users keep every exit (cancel orders, remove liquidity, bank sends, IBC transfers). The list ships empty in v8.2.0; a denom is halted later by a MsgUpdateParams governance proposal. No migration, no ConsensusVersion change, no change under app/upgrades, nothing outside x/tradebin (plus one line in the CI test script, see below). The EndBlock order book engine is untouched.

Ticket: BZE-144 (chain release epic BZE-63). UI counterpart: BZE-145.

Behaviour once a denom is halted

A market or pool is halted iff its base or quote is halted (exact string match; the native denom can never be listed).

  • Refused with ErrDenomHalted (4017) before any fee capture or escrow: MsgCreateMarket, MsgCreateLiquidityPool, MsgCreateOrder, MsgFillOrders, MsgAddLiquidity, and any MsgMultiSwap route touching the denom, also as an intermediate hop.
  • Already queued messages execute as before. The order book queue is only the asynchronous execution stage of messages the msg server already accepted (validated, fee captured, funds escrowed), not a second gate. Since nothing new can enter the queue once the halt is in effect (gov EndBlock precedes tradebin's), the messages accepted in the previous few blocks are matched, saved as resting orders, or refunded exactly as today, and the queue simply drains. The audited engine is not modified.
  • Never swapped, by anyone: Keeper.swapTokens, the single choke point of every swap (user routes, ModuleSwapForNativeDenom, ModuleAddLiquidityWithNativeDenom, the fee payer), refuses a pool holding a halted denom. HasLiquidityWithNativeDenom, HasDeepLiquidityWithNativeDenom and CanSwapForNativeDenom answer false, so the ante handler refuses the denom as a fee denom and the fee collector / burner classify it as non-swappable without attempting a swap.
  • Still working: MsgCancelOrder, MsgRemoveLiquidity, bank sends, IBC transfers, staking / rewards that merely hold the denom, MsgFundBurner. Resting orders and pool reserves are left exactly as they are (no sweep).
  • Un-halting is the same proposal without the denom: new orders and fills are accepted again, pools swap again.

Intended consequences (decided, not side effects)

  • Coins of a halted denom held by other modules (fee dust in the community-pool and burner collectors, expired trading-reward prizes, raffles, MsgFundBurner) are treated like any non-swappable IBC denom and end up locked in the burner black hole. Nothing keeps such coins aside for a later swap.
  • Tx-fee dust in the denom is distributed to stakers in kind.
  • MsgMoveIbcLockedCoins for the denom returns an error.
  • A mempool tx paying fees in the denom at halt time fails at DeliverTx without charging a fee (ReCheckTx skips the denom check, DeliverTx repeats it).
  • A trading reward can still be created on a halted market (governance activates rewards; an unactivated one is burned after 30 days). The dex UI simply does not offer halted markets.

Governance runbook (later, not in this PR)

MsgUpdateParams replaces the whole Params object, so the proposal must carry every current field value plus the new list.

  1. Query the current params and copy every field: bzed query tradebin params (mainnet REST: GET https://rest.getbze.com/bze/tradebin/params).
  2. Proposal with a single /bze.tradebin.MsgUpdateParams message: authority = the gov module account (mainnet bze10d07y265gmmuvt4z0w9aw880jnsr700j8xlwyy), params = the copied values plus halted_denoms: ["<denom>"]. Submit with bzed tx gov submit-proposal proposal.json.
  3. Rehearse on the testnet first with a factory denom that has both a market and a pool: submit, vote, then verify every effect above with real transactions. The same rehearsal is what the dex UI work tests against.
  4. Reverting = the same proposal without the denom.

The runbook is also in x/tradebin/docs/README.md.

Changes

  • proto/bze/tradebin/v2/params.proto, x/tradebin/v2types/params.pb.go, api/bze/tradebin/v2/params.pulsar.go: repeated string halted_denoms = 13.
  • x/tradebin/v2types/params.go: MaxHaltedDenoms = 32, Params.IsDenomHalted (exact match), Validate checks every entry (sdk.ValidateDenom, not the native denom, no duplicates, at most 32). DefaultParams() leaves it empty.
  • x/tradebin/types/errors.go: ErrDenomHalted (4017).
  • x/tradebin/keeper/service_halt.go (new): IsDenomHalted, IsMarketHalted, isMarketHalted, isPoolHalted.
  • x/tradebin/keeper/msg_server_amm.go: validateMarketAssets (covers create market + create pool), AddLiquidity, getRoutesPools refuse halted denoms/pools; MultiSwap surfaces ErrDenomHalted as-is instead of wrapping it in ErrInvalidRoutes.
  • x/tradebin/keeper/msg_server_order_book.go: CreateOrder and FillOrders refuse a halted market right after GetMarketById.
  • x/tradebin/keeper/service_amm.go: swapTokens refuses a halted pool (the no-swap invariant).
  • x/tradebin/keeper/service_denom.go: the three liquidity answers are false for a halted denom.
  • x/tradebin/keeper/service_fee_payer.go: both fee-payer entry points fall back to capturing the native fee when the preferred denom's pool is halted.
  • x/tradebin/docs/{params,README,technical}.md: param, effects, runbook, technical notes.
  • scripts/testing/test-package.sh: the package pattern now also selects v2types — the previous pattern (paths ending in /types) silently skipped x/tradebin/v2types, so its tests never ran in CI.
  • CHANGELOG.md: entries in the open v8.2.0 section.

Not changed: queue_message_processor.go and the events proto. An earlier commit on this branch added an EndBlock refund path for queued messages on halted markets; it was removed in the last commit after review (see the commit message).

Tests

  • x/tradebin/v2types/params_test.go: params table (empty valid, valid list, exactly 32 valid, native denom rejected, duplicate rejected, malformed/empty denom rejected, 33 rejected, DefaultParams() validates, exact-match semantics).
  • x/tradebin/keeper/msg_server_halted_test.go: every listed message returns ErrDenomHalted with no bank expectation registered (gomock panics on an unexpected call, so a fee capture or escrow before the rejection would fail the test); halted pool as first and as second hop; unrelated market / pool unaffected; CancelOrder on a halted market queues normally; RemoveLiquidity on a halted pool pays out normally.
  • x/tradebin/keeper/service_halt_test.go: helpers; the three liquidity answers false for a halted denom with a deep pool and unchanged otherwise; swapTokens (exposed via export_test.go) errors on a halted pool with untouched reserves and swaps again once un-halted; ModuleSwapForNativeDenom errors and moves nothing; ModuleAddLiquidityWithNativeDenom refunds the coin; both fee-payer entry points fall back to the native fee.
  • x/tradebin/module/halted_denoms_wiring_test.go: MsgUpdateParams through the real msg service router sets and clears the list from the gov authority, a non-authority signer and a list naming the native denom are rejected, genesis export/import round-trips and validates.
  • The existing engine suite runs unchanged (the engine code is unchanged).

Every behaviour test fails on the pre-change keeper code (production guards reverted, tests kept):

--- FAIL: TestKeeperSuite/TestMsgHalted_AddLiquidity_Rejected
--- FAIL: TestKeeperSuite/TestMsgHalted_CreateLiquidityPool_Rejected
--- FAIL: TestKeeperSuite/TestMsgHalted_CreateMarket_Rejected
--- FAIL: TestKeeperSuite/TestMsgHalted_CreateOrder_Rejected
--- FAIL: TestKeeperSuite/TestMsgHalted_FillOrders_Rejected
--- FAIL: TestKeeperSuite/TestMsgHalted_MultiSwap_Rejected
--- FAIL: TestKeeperSuite/TestServiceHalt_FeePayer_PreferredHaltedDenom_FallsBackToNative
--- FAIL: TestKeeperSuite/TestServiceHalt_LiquidityAnswers_FalseForHaltedDenom
--- FAIL: TestKeeperSuite/TestServiceHalt_ModuleAddLiquidityWithNativeDenom_RefundsHaltedCoin
--- FAIL: TestKeeperSuite/TestServiceHalt_ModuleSwapForNativeDenom_RefusesHaltedDenom
--- FAIL: TestKeeperSuite/TestServiceHalt_SwapTokens_RefusesHaltedPool

Verified locally: go build ./..., gofmt, go vet ./x/tradebin/..., ./scripts/testing/test-package.sh (all packages, -race).

A repeated string param, empty by default, that governance sets through
MsgUpdateParams. Validate accepts at most 32 well-formed, distinct denoms
and never the native denom (halting it would halt every market and pool).
Params.IsDenomHalted is an exact string comparison, so market and pool
ids that merely contain a denom never match.

No migration and no consensus version bump: a new proto3 repeated field
decodes as empty from the params bytes already stored on chain.

The CI test script now also selects tradebin's v2types package, which the
previous pattern (paths ending in /types) silently skipped.
A market or pool whose base or quote is halted now rejects
MsgCreateMarket, MsgCreateLiquidityPool, MsgCreateOrder, MsgFillOrders,
MsgAddLiquidity and any MsgMultiSwap route touching it (also as an
intermediate hop) with the new ErrDenomHalted (4017), before any fee is
captured or funds are escrowed. MsgCancelOrder and MsgRemoveLiquidity are
untouched: exits always work.

The no-swap invariant is enforced at Keeper.swapTokens, the single choke
point every swap goes through, so user routes, ModuleSwapForNativeDenom,
ModuleAddLiquidityWithNativeDenom and the fee payer all refuse a halted
pool at once. HasLiquidityWithNativeDenom, HasDeepLiquidityWithNativeDenom
and CanSwapForNativeDenom answer false for a halted denom, so the ante
handler refuses it as a fee denom and the fee collector and burner
classify it as non-swappable without attempting a swap. The fee payer
falls back to capturing the native fee when the preferred denom's pool is
halted. Nothing changes in txfeecollector, burner or rewards.
The processing engine asks the keeper whether a message's market is
halted before dispatching it. A buy, sell, fill_buy or fill_sell on a
halted market is refunded in full to its owner through the existing
refundMessageFunds (the cancel coin math, dust stored), emits the new
QueueMessageRefundedEvent with reason "market_halted" and leaves the
queue like any processed message; cancels are processed unchanged.

Gov's EndBlock runs before tradebin's, so a passed halt proposal already
applies to the queue in that block, and any backlog beyond
order_book_per_block_messages is refunded over the following blocks.
Resting orders and aggregates are never touched.
Drives MsgUpdateParams through the real msg service router: the gov
authority sets and clears halted_denoms, a non-authority signer and a
list naming the native denom are rejected, and the stored list survives
a genesis export/import. Documents the param, the effects of a halt and
the governance runbook in the tradebin docs.
… as before

Revised on review: the order book queue is only the asynchronous
execution stage of messages the msg server already accepted (validated,
fee captured, funds escrowed), not a second gate. The msg-server guards
stop any new message from entering the queue once a halt is in effect,
so the few messages queued in the previous blocks can safely execute as
always and the queue simply drains. The audited engine stays as it is.

Drops the halted-market refund branch from the processing engine, the
IsMarketHalted requirement on ProcessingKeeper, the
QueueMessageRefundedEvent proto and generated code, and the engine test.
Docs and changelog now say already-queued messages execute normally.
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.

1 participant