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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add PlamenTSV/plamen --skill cosmos-sdk-module-safety --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cosmos Sdk Module Safety?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/plamentsv-cosmos-sdk-module-safety)More formats (shields.io, HTML) on the badges page.
---
name: "cosmos-sdk-module-safety"
description: "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=true` AND `COSMOS_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**:
1. From `caller_map.md` / `function_summary.md`, enumerate every function reachable from a `Msg` handler, `BeginBlock(er)`, `EndBlock(er)`, `PreBlocker`, `InitGenesis`, or Ante/Post handler. These are the consensus-critical set.
2. For each, grep / inspect for non-deterministic sources:
- **Map-range iteration without sort**: `for k := range <map>` or `for 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 from `ctx.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` / `float64` arithmetic, `math.Pow`, `math.Sqrt` on consensus values — float results are not guaranteed bit-identical across platforms; use `sdkmath.LegacyDec` / `big.Int`.
- **`unsafe.`**, `reflect`-driven ordering, goroutines/channels whose completion order affects state, and `select` over multiple ready channels.
3. 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**:
1. Locate every `BeginBlock(er)` / `EndBlock(er)` / `PreBlocker` (grep `func.*BeginBlock`, `func.*EndBlock`, `module.go`, `abci.go`).
2. 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).
3. Confirm the absence of a per-block work cap (a `maxPerBlock` limit, 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**:
1. For each proto `Msg` type, find its `GetSigners()` (generated or hand-written) and record the signer-deriving field(s) (commonly `Creator`, `Sender`, `FromAddress`, `Authority`).
2. From `state_write_map.md`, list every field the corresponding `msgServer` handler writes or whose owner it changes.
3. 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 `owner` without checking it equals a signer.
- An `Authority`/governance gate is declared but the handler never compares the signer to it.
4. For `x/authz`-style delegated execution, verify the grant is checked for the exact `Msg` type 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**:
1. Identify module accounts (`authtypes.NewModuleAddress`, `GetModuleAccount`, `RegisterModuleAccount`, permissions `Minter`/`Burner`/`Staking`).
2. Enumerate every credit/debit path: `bankKeeper.SendCoinsFromAccountToModule`, `SendCoinsFromModuleToAccount`, `MintCoins`, `BurnCoins`, and any **direct** `bankKeeper.SendCoins(...)` touching a module address.
3. Flag direct `SendCoins` / `SendCoinsFromModuleToModule` that 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).
4. Verify (or recommend) a registered `InvariantRoute` (`RegisterInvariants`) asserting `sum(internal) == moduleBalance`. Absence of the invariant plus a bypassing send path is the finding.
5. Check mint/burn symmetry: every `MintCoins` that increases supply must have an accounting record; every `BurnCoins` must 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**:
1. Grep for chained decimal math: `.Quo(`, `.Mul(`, `.QuoInt(`, `.MulInt(`, `.QuoTruncate(`, `.MulTruncate(`, `.RoundInt(`, `.TruncateInt(`.
2. For each `(a Quo b) Mul c` (or `Mul`-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.
3. Verify rounding-direction consistency between paired operations: deposit→shares and shares→withdraw must round in protocol-favorable opposite directions so `withdraw(deposit(x)) <= x`.
4. 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**:
1. 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 `, ok` form, and `sdk.NewCoins(...)`/`coins.Add(...)`/`total.Add(...)` over **unsorted or unvalidated** denoms (which panics on duplicate/invalid denom).
2. For each, trace whether the input can be attacker-controlled (a `Msg` field, 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.
3. Distinguish: panics in `BeginBlock`/`EndBlock` are 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.
4. Verify `ValidateBasic` actually 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**:
1. Enumerate every `Msg*` RPC from the proto / `tx.pb.go` `MsgServer` interface (and any legacy `sdk.Msg` types).
2. Cross-check registration: `RegisterMsgServer` / `_Msg_serviceDesc`, `RegisterServices`, legacy `NewHandler` switch arms, and `RegisterLegacyAminoCodec` / `RegisterInterfaces` for the concrete type.
3. Flag any `Msg*` with no `msgServer` method, no route arm, or unregistered concrete type → the message is unhandled / un-decodable.
4. Also flag the inverse mismatch: a handler that exists but whose `Msg` type 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**:
1. Grep for `uint64` multiplication/addition in fee/gas paths: `GasUsed`, `GasWanted`, `gasPrice`, `baseFee`, `Fee.Amount`, AnteHandler fee deduction, refund/`ConsumeGas` accounting.
2. For each `a * b` / `a + b` on `uint64` where either operand is message- or block-controlled, check for an overflow guard (use of `sdkmath.Int` / `big.Int`, or an explicit `math.MaxUint64 / b` pre-check).
3. Verify refund <= escrowed: a refund computed by subtraction must guard against underflow (refund > paid).
4. Recommend migrating value-bearing fee/gas math to `sdkmath.Int` (arbitrary precision) rather than raw `uint64`.
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`, and `function_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 `keeper` method) ON-DEMAND only when a graph artifact flags it — never read an entire `x/<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.
1. **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.
2. **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-verification` skill; this skill flags the Cosmos-side `Msg`/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).
3. **Module-account drain class** — a keeper that pays out via direct `bankKeeper.SendCoins` from the module account without decrementing the per-user ledger, so `sum(internal) > moduleBalance` and later claimants are bricked, or the inverse over-issuance. **Catch point**: Section 4.
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.
5. **AnteHandler fee-overflow class** — `gasPrice * gasWanted` overflowing `uint64` to 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`, and `app/app.go`.
- Grep for `func.*BeginBlock`, `func.*EndBlock`, `func.*PreBlocker` (consensus hooks).
- Grep `for .* := range ` and intersect with handler/hook files (non-determinism candidates).
- Grep `GetSigners`, `SendCoins`, `MintCoins`, `BurnCoins`, `panic(`, `.Quo(`, `.Mul(`, `RegisterMsgServer`, `RegisterServices`.
- Read `proto/**/tx.proto` for the full `Msg` service 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`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!