fix - BZE-142 - hooks reach the msg servers, panic-safe distribution queues, DR genesis value checks - #113
Merged
Conversation
RegisterServices runs inside appBuilder.Build, before app.go wires any hooks, so the by-value keeper copy embedded in the msg server froze the nil hooks at registration time and Keeper.SetHooks could never reach JoinStaking, ExitStaking or DeleteStakingReward. Embed *Keeper instead and pass the module's pointer; tests no longer rebuild the msg server after SetHooks. A module-level test dispatches MsgJoinStaking through the real msg service router with hooks registered after RegisterServices.
ProcessStakingRewardsDistributionQueue and ProcessDenomRewardsDistributionQueue ran every per-entry payout unguarded, so a corrupt record or an accumulator overflow would have halted the chain. Each entry now runs through bzeutils.ApplyFuncIfNoError (cache context + recover), matching the unlock and trading-reward queues: the bad entry is logged and skipped with its writes discarded, the rest of the batch is paid and the queue keeps draining. Tests inject a panic into one entry of each queue (epoch mock for DR, a LegacyDec overflow for SR) and assert the others are paid, the marker write made before the panic is rolled back and the queue drains. Existing queue tests match the epoch read on any context since it now runs on the cache one.
validateDenomRewards accepted any numeric value as long as the references resolved: negative stakes and accumulators, participant amounts that do not sum to the pool's staked amount, indexes ahead of their prize accumulator, zero-amount or zero-duration schedules and schedules with payouts >= duration (re-enqueued every day, never finishing), and duplicate records (last-wins on import). None can come from an export; all can come from a hand-edited genesis and some strand escrow or wedge the daily pass. Reject them in validate-genesis. Deliberately not enforced: the prize-denom cap (governance may lower the param below an existing pool's count, so a real export can exceed it) and indexes without a participant record. Absent JSON numeric fields still count as zero; the absent-fields keeper test now expects the rejection while still pinning the panic-free import path.
busydonna
marked this pull request as ready for review
September 11, 2026 13:44
Same defect as the rewards msg server: RegisterServices (inside appBuilder.Build) copied the keeper by value before app.go registered the order-fill hooks, so the AMM swap path (MsgMultiSwap -> onSwapSuccess) read an empty hook slice forever while the orderbook path, whose ProcessingEngine is built from the keeper pointer each block, saw them. Embed *Keeper and pass the module's pointer. No on-chain change with the current hook: the rewards hook resolves trading rewards by orderbook market id (base/quote) and AMM swaps report the pool id (base_quote), so the lookup misses either way. Module-level tests dispatch a MsgMultiSwap through the real msg service router with hooks registered after RegisterServices (the app's order), before it (the v7 order), and check the msg server shares the keeper; the first and last fail on the previous wiring.
busydonna
marked this pull request as draft
September 11, 2026 13:48
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.
Summary
Four independent hardening items that came out of the v8.2.0 release review (BZE-142): three in
x/rewardsplus the same msg-server wiring fix inx/tradebin. None changes on-chain behaviour under current mainnet conditions; they close latent defects. Each item is its own commit so it can be cherry-picked into v8.2.0 or deferred on its own:RegisterServices(insideappBuilder.Build), beforeapp.gowires any hooks, soKeeper.SetHookscould never reachJoinStaking/ExitStaking/DeleteStakingReward. Not consensus-affecting: no module registers these hooks today.bzeutils.ApplyFuncIfNoError(cache context + recover), like the unlock and trading-reward queues. Behaviour differs only on the panic path (skip and log instead of halting every node), so it is listed under State Machine Breaking and must activate with the upgrade handler.validate-genesisnow rejects duplicates, negative stakes/accumulators, participant amounts that do not sum to the pool's stake, indexes ahead of their accumulator, and zombie schedules. Only genesis validation changes; every chain export still passes (the existing keeper round-trip tests are the guard).base/quote) while AMM swaps report the pool id (base_quote), so the lookup misses either way.Changes
Commit 1 — share the keeper pointer with the rewards msg server
x/rewards/keeper/msg_server.go:msgServerembeds*Keeper;NewMsgServerImpl(k *Keeper).x/rewards/module/module.go:RegisterServicespassesam.keeper(pointer); corrected the comment that claimed the pointer already made hooks visible.x/rewards/keeper/*_test.go: call sites pass the keeper pointer;registerHooksno longer rebuilds the msg server afterSetHooks.x/rewards/module/hooks_wiring_test.go(new): registers services through the real SDK configurator, callsSetHooksafterwards (the app's order) and dispatchesMsgJoinStakingthrough the msg service router; the hook must fire. Fails on the previous wiring.x/rewards/module/migration_wiring_test.go: fixture keeper is a pointer andmanager()also returns the msg service router.CHANGELOG.md: Bug Fixes entry.Commit 2 — recover from panics in the EndBlock distribution queues
x/rewards/keeper/service_denom_reward.go:safeDistributeDenomRewardSchedulewraps each schedule's pass; a panic is logged with the schedule id and the cursor keeps moving.x/rewards/keeper/service_staking_reward.go: same treatment forProcessStakingRewardsDistributionQueue(safeDistributeStakingReward).x/rewards/keeper/service_distribution_panic_safety_test.go(new): one entry of each queue panics (epoch mock for DR, a genuineLegacyDecoverflow for SR); the other entries are paid, a marker written before the panic is rolled back, the queue drains, and the next day pays the skipped entry.x/rewards/keeper/service_denom_reward_distribution*_test.go: epoch mock matches any context, since the read now happens on the cache context.CHANGELOG.md: State Machine Breaking entry.Commit 3 — validate denom reward genesis values, not only references
x/rewards/types/genesis.go:validateDenomRewardsadds duplicate detection for prizes / participants / indexes / schedules, non-negative staked amounts and accumulators, positive participant amounts summing to the pool's staked amount, indexes never ahead of the accumulator, schedules pointing at an existing prize with a positive daily amount, non-zero duration andpayouts < duration. Absent JSON numeric fields still count as zero. Deliberately not enforced: the prize-denom cap (governance may lower the param below an existing pool's count, so a real export can exceed it) and indexes without a participant record.x/rewards/types/genesis_test.go: one table case per rule plus three positive cases (nil numerics, prizes above the cap, index without participant); 35 cases total.x/rewards/keeper/genesis_absent_fields_test.go: the hand-edited genesis with absent amounts is now rejected by validation; the test still pins the panic-free import path.CHANGELOG.md: Improvements entry.Commit 4 — share the keeper pointer with the tradebin msg server
x/tradebin/keeper/msg_server.go:msgServerembeds*Keeper;NewMsgServerImpl(k *Keeper).x/tradebin/module/module.go:RegisterServicespassesam.keeper(pointer).x/tradebin/keeper/keeper_test.go: suite passes the keeper pointer.x/tradebin/module/hooks_wiring_test.go(new): three tests through the real SDK configurator and msg service router — hooks registered afterRegisterServices(the app's order) fire on aMsgMultiSwap; hooks registered before (the pre-v8 order) fire too; the msg server sees hooks registered after it was built. The first and the last fail on the previous wiring.CHANGELOG.md: Bug Fixes entry.Verification
./scripts/testing/test-package.sh(the CI set,-race) green.go build ./...andgo vet ./x/rewards/...clean.