plamentsv / cosmos-sdk-module-safety
L1 trigger - audits Cosmos-SDK / CometBFT modules for consensus non-determinism, unmetered ABCI hooks, signer/state mismatches, module-account bookkeeping breaks, sdk.Dec rounding, ABCI-path panics, unregistered Msg handlers, and fee/gas overflow.
Injectable Skill: Cosmos-SDK / CometBFT Module Safety
L1 trigger:
L1_PATTERN=trueANDCOSMOS_SDK(cosmos-sdk / cometbft / tendermint /x/modules detected)
Inject Into:depth-consensus-invariant,depth-state-trace
Language: Go (Cosmos-SDK)
Finding prefix:[COS-N]
Status: v0.1
Orchestrator Decomposition Guide
- Sections 1, 2, 6, 7: depth-consensus-invariant (determinism, ABCI-hook metering, ABCI panics, Msg routing — all chain-halt / safety class)
- Sections 3, 4, 5, 8: depth-state-trace (signer/state authority, module-account invariant, Dec rounding, fee/gas overflow — all accounting / authorization class)
- A single bug can span both lenses; record it once and cross-reference.
When This Skill Activates
Recon classifies the target as a Cosmos-SDK / CometBFT application chain or module set (go.mod requires cosmossdk.io/..., github.com/cosmos/cosmos-sdk, github.com/cometbft/cometbft, github.com/tendermint/tendermint, or the tree has x/<module>/keeper, x/<module>/types, abci.go, module.go). Cosmos app-chain bugs are predominantly consensus-halting (every validator must compute byte-identical state) and accounting (module accounts must reconcile) — both are in scope for Plamen L1 audits. Severity baseline is Medium; chain-halt / fork and fund-loss classes upgrade to High/Critical per docs/l1-mode/severity-matrix.md.
A Cosmos state-transition path is any code reachable from a Msg handler (msgServer methods), BeginBlock, EndBlock, InitGenesis, EndBlocker, PreBlocker, or an AnteHandler/PostHandler that mutates committed state. Everything in those paths runs on every validator and MUST be deterministic and panic-safe.
1. Non-Determinism in State-Transition Paths
Check: No state-transition path may depend on per-node, wall-clock, or unordered data. Two honest validators replaying the same block MUST compute the same app hash; any divergence forks or halts the chain.
Methodology:
- From
caller_map.md/function_summary.md, enumerate every function reachable from aMsghandler,BeginBlock(er),EndBlock(er),PreBlocker,InitGenesis, or Ante/Post handler. These are the consensus-critical set. For each, grep / inspect for non-deterministic sources:
- Map-range iteration without sort:
for k := range <map>orfor k, v := range <map>where iteration order affects writes, accumulation order, or emitted state. Go randomizes map iteration order per process. Fix pattern is a sorted key slice (maps.Keys+slices.Sort) before ranging. Flag any range-over-map in the consensus set that feeds a state write or running total. - Wall clock:
time.Now(),time.Since(,time.Tick,os.Getenv— block time must come fromctx.BlockTime()/ctx.BlockHeader().Time, never the OS clock. - Randomness:
math/rand,crypto/rand,rand.Intn, unseeded or per-node-seeded RNG in a handler. - Floating point:
float32/float64arithmetic,math.Pow,math.Sqrton consensus values — float results are not guaranteed bit-identical across platforms; usesdkmath.LegacyDec/big.Int. unsafe.,reflect-driven ordering, goroutines/channels whose completion order affects state, andselectover multiple ready channels.
- Map-range iteration without sort:
- For each hit, confirm the value actually feeds committed state (app hash). A non-deterministic value used only for a log line is not a finding; one used for a balance, a winner selection, an iteration order over payouts, or an emitted event consumed by light clients is.
Tag: [COS-NONDET:{source}:{file}:{line}→{state-effect}]
2. Unmetered BeginBlock / EndBlock Hooks
Check: ABCI hooks (BeginBlock, EndBlock, BeginBlocker, EndBlocker, PreBlocker) run every block with no per-message gas meter bounding them. An unbounded or super-linear loop over state that an attacker can grow turns block production into a DoS / liveness failure.
Methodology:
- Locate every
BeginBlock(er)/EndBlock(er)/PreBlocker(grepfunc.*BeginBlock,func.*EndBlock,module.go,abci.go). Inside each, walk loops. Flag:
- Iteration over a
GetAll*/IterateAll*/ full-store iterator whose element count grows with user actions (e.g. all unbonding entries, all open orders, all proposals, all accounts) with no cap per block. - Nested loops over growing state (O(n²)) — e.g. for each validator, for each delegation.
len(slice)/ iterator length driven by attacker-controlled creation (an attacker spams cheap objects; the hook then iterates all of them every block).
- Iteration over a
- Confirm the absence of a per-block work cap (a
maxPerBlocklimit, a paginated queue drained N-at-a-time, or a time-bounded window). Absence with attacker-growable input is the finding.
Tag: [COS-ABCI-UNMETERED:{hook}:{file}:{line}:{growth-driver}]
3. GetSigners vs State-Modifying Field Mismatch
Check: A Msg may only mutate state owned by an account that is in its authenticated signer set. If a handler writes a field (owner, recipient, admin, target address) that is NOT derived from msg.GetSigners() (or the SDK-validated signer), an attacker can act on behalf of, or against, another account.
Methodology:
- For each proto
Msgtype, find itsGetSigners()(generated or hand-written) and record the signer-deriving field(s) (commonlyCreator,Sender,FromAddress,Authority). - From
state_write_map.md, list every field the correspondingmsgServerhandler writes or whose owner it changes. Cross-check: every account/address the handler treats as authorized MUST be in the signer set. Flag when:
- The handler reads an address from a message field NOT in
GetSigners()and then debits/credits/reassigns it. - The handler trusts a stored object's
ownerwithout checking it equals a signer. - An
Authority/governance gate is declared but the handler never compares the signer to it.
- The handler reads an address from a message field NOT in
- For
x/authz-style delegated execution, verify the grant is checked for the exactMsgtype and granter.
Tag: [COS-SIGNER-MISMATCH:{msg}:{written-field}:{file}:{line}]
4. Module-Account Bookkeeping Invariant
Check: For every module that holds funds, sum(per-user balances / deposits / shares tracked in module state) == bankKeeper.GetBalance(moduleAccount, denom) must hold after each block. Direct bank sends that bypass the module's own accounting break this silently.
Methodology:
- Identify module accounts (
authtypes.NewModuleAddress,GetModuleAccount,RegisterModuleAccount, permissionsMinter/Burner/Staking). - Enumerate every credit/debit path:
bankKeeper.SendCoinsFromAccountToModule,SendCoinsFromModuleToAccount,MintCoins,BurnCoins, and any directbankKeeper.SendCoins(...)touching a module address. - Flag direct
SendCoins/SendCoinsFromModuleToModulethat move module funds WITHOUT a paired update to the module's internal ledger (the per-user mapping the module uses to compute who is owed what). - Verify (or recommend) a registered
InvariantRoute(RegisterInvariants) assertingsum(internal) == moduleBalance. Absence of the invariant plus a bypassing send path is the finding. - Check mint/burn symmetry: every
MintCoinsthat increases supply must have an accounting record; everyBurnCoinsmust decrement the corresponding internal claim.
Tag: [COS-MODACCT-INVARIANT:{module}:{bypass-path}:{file}:{line}]
5. sdk.Dec / LegacyDec Rounding Non-Associativity
Check: sdkmath.LegacyDec (sdk.Dec) operations lose precision; the order of chained Quo/Mul changes the result, and rounding must always favor the protocol (never the user) to prevent dust extraction or under-collateralization.
Methodology:
- Grep for chained decimal math:
.Quo(,.Mul(,.QuoInt(,.MulInt(,.QuoTruncate(,.MulTruncate(,.RoundInt(,.TruncateInt(. - For each
(a Quo b) Mul c(orMul-then-Quo), check whether reordering changes the rounded result and whether the chosen order favors the user (e.g. rounding a withdrawal UP or a deposit-share DOWN). User-favorable rounding in a value-bearing path is a finding. - Verify rounding-direction consistency between paired operations: deposit→shares and shares→withdraw must round in protocol-favorable opposite directions so
withdraw(deposit(x)) <= x. - Boundary-fuzz: substitute edge denoms (1 base unit, very large supply, dust amounts) and small exchange rates; precision loss is largest at extremes. Recommend a property test asserting the no-free-value invariant.
Tag: [COS-DEC-ROUNDING:{op-chain}:{file}:{line}:{who-favored}]
6. ABCI / Consensus-Path Panics
Check: A panic in DeliverTx (a Msg handler), BeginBlock, EndBlock, or PreBlocker is not a graceful tx failure — it can crash validators or, depending on recovery placement, cause inconsistent rollback and a chain halt. Handler errors must be returned as error, not raised as panic.
Methodology:
- In the consensus set (Section 1 step 1), grep for
panic(,.MustMarshal,Must*(helpers, unchecked array/slice indexing on message-controlled length, integer division by a message-controlled denominator, type assertions.(T)without the, okform, andsdk.NewCoins(...)/coins.Add(...)/total.Add(...)over unsorted or unvalidated denoms (which panics on duplicate/invalid denom). - For each, trace whether the input can be attacker-controlled (a
Msgfield, a peer-supplied value, a genesis import). Attacker-reachable panic in a handler = at least a tx-griefing finding; in BeginBlock/EndBlock = liveness / chain-halt class. - Distinguish: panics in
BeginBlock/EndBlockare the most severe (no per-tx recovery boundary). A panic recovered by the baseapp tx middleware that still corrupts a cached store before recovery is also a finding. - Verify
ValidateBasicactually rejects the input class that would otherwise panic later (stateless validation is the correct place to bound lengths/denoms).
Tag: [COS-ABCI-PANIC:{path}:{panic-site}:{file}:{line}]
7. Missing / Unregistered Msg Handlers
Check: A proto Msg type with no route or no registered service handler silently fails (the tx is accepted into a block but the state change never happens, or it is rejected in a way that diverges from the spec). Every declared Msg* must be wired to exactly one handler.
Methodology:
- Enumerate every
Msg*RPC from the proto /tx.pb.goMsgServerinterface (and any legacysdk.Msgtypes). - Cross-check registration:
RegisterMsgServer/_Msg_serviceDesc,RegisterServices, legacyNewHandlerswitch arms, andRegisterLegacyAminoCodec/RegisterInterfacesfor the concrete type. - Flag any
Msg*with nomsgServermethod, no route arm, or unregistered concrete type → the message is unhandled / un-decodable. - Also flag the inverse mismatch: a handler that exists but whose
Msgtype is not registered in the interface registry (decoding fails) — and duplicate routes (two arms for one type).
Tag: [COS-MSG-UNROUTED:{msg-type}:{registration-gap}]
8. Fee / Gas uint64 Overflow
Check: Fee and gas arithmetic on uint64 (baseFee * gasUsed, refund computations, gas-price multiplication) can overflow and wrap, producing a near-zero fee, a refund larger than the escrowed amount, or an out-of-bounds gas grant.
Methodology:
- Grep for
uint64multiplication/addition in fee/gas paths:GasUsed,GasWanted,gasPrice,baseFee,Fee.Amount, AnteHandler fee deduction, refund/ConsumeGasaccounting. - For each
a * b/a + bonuint64where either operand is message- or block-controlled, check for an overflow guard (use ofsdkmath.Int/big.Int, or an explicitmath.MaxUint64 / bpre-check). - Verify refund <= escrowed: a refund computed by subtraction must guard against underflow (refund > paid).
- Recommend migrating value-bearing fee/gas math to
sdkmath.Int(arbitrary precision) rather than rawuint64.
Tag: [COS-FEE-OVERFLOW:{op}:{file}:{line}]
Bounded Reads
MANDATORY — do not bulk-read large source files (context-collapse risk). Drive analysis from the Go SCIP bake graph artifacts first, then open individual symbols on demand:
- Read
caller_map.md,callee_map.md,state_write_map.md, andfunction_summary.md(produced by the Go SCIP bake) to build the consensus-critical reachability set and the per-field writer set without reading whole modules. - Open an individual source symbol (one function / one
keepermethod) ON-DEMAND only when a graph artifact flags it — never read an entirex/<module>tree or a multi-thousand-line file in one go. - If a needed symbol is not in the graph artifacts, grep for the specific function/identifier and read only the matched span plus a few lines of context.
- Write enumerations (consensus set, signer map, module-account paths) to small scratchpad notes; do not hold whole files in context.
Output schema
- Layer: consensus (Sections 1, 2, 6, 7) / state (Sections 3, 4, 5, 8)
- Bug class: non-determinism / unmetered-abci / signer-mismatch / module-account-invariant / dec-rounding / abci-panic / msg-unrouted / fee-overflow
- Finding prefix:
[COS-N] - Preferred evidence tags:
[FUZZ-PASS](proptest/Go fuzz on the handler or Dec math) >[NON-DET-PASS](replay differential showing divergent app hash) >[CODE-TRACE] - Severity baseline: Medium; upgrade to High/Critical for chain-halt / fork (Sections 1, 2, 6) or fund loss (Sections 4, 5, 8); see
docs/l1-mode/severity-matrix.md
Known bug exemplars
Illustrative bug CLASSES only — methodology finds these generically, the names are public-incident exemplars, not protocol targets.
- Go map-iteration non-determinism class — ranging over a Go map in a handler / BeginBlock and using iteration order to write state or accumulate a total. Because Go randomizes map order per process, validators diverge → app-hash mismatch → chain halt. Multiple Cosmos chains have shipped and patched this class. Catch point: Section 1 map-range check.
- ICS-23 / Merkle proof-gap class (Dragonberry-class) — a missing or malformed membership/non-membership proof check lets a forged value pass verification. (Cross-chain proof verification primarily lives in the
light-client-proof-verificationskill; this skill flags the Cosmos-sideMsg/keeper entry points that consume such proofs and the signer/authority gating around them.) Catch point: Sections 3 and 6 (entry-point authority + panic-safety on proof inputs). - Module-account drain class — a keeper that pays out via direct
bankKeeper.SendCoinsfrom the module account without decrementing the per-user ledger, sosum(internal) > moduleBalanceand later claimants are bricked, or the inverse over-issuance. Catch point: Section 4. - Unmetered EndBlock loop class — iterating all of an attacker-growable collection (open orders, unbonding queue) every block with no cap, so cheap spam linearly inflates block time. Catch point: Section 2.
- AnteHandler fee-overflow class —
gasPrice * gasWantedoverflowinguint64to a tiny fee, defeating fee-market DoS protection. Catch point: Section 8.
Fallback
If the SCIP graph artifacts are unavailable or incomplete:
- List
x/*/keeper/*.go,x/*/abci.go,x/*/module.go, andapp/app.go. - Grep for
func.*BeginBlock,func.*EndBlock,func.*PreBlocker(consensus hooks). - Grep
for .* := rangeand intersect with handler/hook files (non-determinism candidates). - Grep
GetSigners,SendCoins,MintCoins,BurnCoins,panic(,.Quo(,.Mul(,RegisterMsgServer,RegisterServices. - Read
proto/**/tx.protofor the fullMsgservice to enumerate handlers for Section 7.
Cross-references
- Related:
consensus-safety-invariants(general non-determinism / state-transition completeness),go-concurrency-safety(Go map / goroutine hazards),light-client-proof-verification(ICS-23 / Merkle proof soundness),validator-lifecycle-and-slashing(x/staking, x/slashing lifecycle),dependency-audit-nodeclient(cosmos-sdk / cometbft version CVEs). - Consumed by:
depth-consensus-invariant,depth-state-trace - Severity:
docs/l1-mode/severity-matrix.md