agents/skills/injectable/l1/cosmos-sdk-module-safety/SKILL.md
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.
npx skillsauth add plamentsv/plamen cosmos-sdk-module-safetyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
L1 trigger:
L1_PATTERN=trueANDCOSMOS_SDK(cosmos-sdk / cometbft / tendermint /x/modules detected) Inject Into:depth-consensus-invariant,depth-state-traceLanguage: Go (Cosmos-SDK) Finding prefix:[COS-N]Status: v0.1
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.
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:
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.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.time.Now(), time.Since(, time.Tick, os.Getenv — block time must come from ctx.BlockTime() / ctx.BlockHeader().Time, never the OS clock.math/rand, crypto/rand, rand.Intn, unseeded or per-node-seeded RNG in a handler.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.Tag: [COS-NONDET:{source}:{file}:{line}→{state-effect}]
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:
BeginBlock(er) / EndBlock(er) / PreBlocker (grep func.*BeginBlock, func.*EndBlock, module.go, abci.go).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.len(slice) / iterator length driven by attacker-controlled creation (an attacker spams cheap objects; the hook then iterates all of them every block).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}]
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:
Msg type, find its GetSigners() (generated or hand-written) and record the signer-deriving field(s) (commonly Creator, Sender, FromAddress, Authority).state_write_map.md, list every field the corresponding msgServer handler writes or whose owner it changes.GetSigners() and then debits/credits/reassigns it.owner without checking it equals a signer.Authority/governance gate is declared but the handler never compares the signer to it.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}]
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:
authtypes.NewModuleAddress, GetModuleAccount, RegisterModuleAccount, permissions Minter/Burner/Staking).bankKeeper.SendCoinsFromAccountToModule, SendCoinsFromModuleToAccount, MintCoins, BurnCoins, and any direct bankKeeper.SendCoins(...) touching a module address.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).InvariantRoute (RegisterInvariants) asserting sum(internal) == moduleBalance. Absence of the invariant plus a bypassing send path is the finding.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}]
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:
.Quo(, .Mul(, .QuoInt(, .MulInt(, .QuoTruncate(, .MulTruncate(, .RoundInt(, .TruncateInt(.(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.withdraw(deposit(x)) <= x.Tag: [COS-DEC-ROUNDING:{op-chain}:{file}:{line}:{who-favored}]
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:
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).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.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.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}]
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:
Msg* RPC from the proto / tx.pb.go MsgServer interface (and any legacy sdk.Msg types).RegisterMsgServer / _Msg_serviceDesc, RegisterServices, legacy NewHandler switch arms, and RegisterLegacyAminoCodec / RegisterInterfaces for the concrete type.Msg* with no msgServer method, no route arm, or unregistered concrete type → the message is unhandled / un-decodable.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}]
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:
uint64 multiplication/addition in fee/gas paths: GasUsed, GasWanted, gasPrice, baseFee, Fee.Amount, AnteHandler fee deduction, refund/ConsumeGas accounting.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).sdkmath.Int (arbitrary precision) rather than raw uint64.Tag: [COS-FEE-OVERFLOW:{op}:{file}:{line}]
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:
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.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.[COS-N][FUZZ-PASS] (proptest/Go fuzz on the handler or Dec math) > [NON-DET-PASS] (replay differential showing divergent app hash) > [CODE-TRACE]docs/l1-mode/severity-matrix.mdIllustrative bug CLASSES only — methodology finds these generically, the names are public-incident exemplars, not protocol targets.
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).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.gasPrice * gasWanted overflowing uint64 to a tiny fee, defeating fee-market DoS protection. Catch point: Section 8.If the SCIP graph artifacts are unavailable or incomplete:
x/*/keeper/*.go, x/*/abci.go, x/*/module.go, and app/app.go.func.*BeginBlock, func.*EndBlock, func.*PreBlocker (consensus hooks).for .* := range and intersect with handler/hook files (non-determinism candidates).GetSigners, SendCoins, MintCoins, BurnCoins, panic(, .Quo(, .Mul(, RegisterMsgServer, RegisterServices.proto/**/tx.proto for the full Msg service to enumerate handlers for Section 7.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).depth-consensus-invariant, depth-state-tracedocs/l1-mode/severity-matrix.mddata-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger STABLESWAP_FORK flag (fork-ancestry detects Curve/StableSwap parent via get_d/get_y/ramp_a/StableSwap patterns) - Agent Type general-purpose (standalone niche agent, 1 budget slot)