example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
Detect bug classes specific to CosmWasm (Rust) contracts — missing info.sender authorization in execute handlers, unbounded map iteration → gas/DoS, reply/submessage reply_id confusion, migrate admin backdoors, addr_validate vs raw string addresses, unchecked info.funds, Uint128
$ npx -y skills add omermaksutii/RugProof --skill cosmwasm --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/cosmwasmContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect bug classes specific to CosmWasm (Rust) contracts — missing info.sender authorization in execute handlers, unbounded map iteration → gas/DoS, reply/submessage reply_id confusion, migrate admin backdoors, addr_validate vs raw string addresses, unchecked info.funds, Uint128
name: cosmwasm description: Detect bug classes specific to CosmWasm (Rust) contracts — missing info.sender authorization in execute handlers, unbounded map iteration → gas/DoS, reply/submessage reply_id confusion, migrate admin backdoors, addr_validate vs raw string addresses, unchecked info.funds, Uint128 overflow, query reentrancy, and migration/version state. Activate on any `.rs` file with `use cosmwasm_std`, `#[entry_point]`, `ExecuteMsg`, `InstantiateMsg`, `QueryMsg`, `cw_storage_plus`, or `DepsMut`.
CosmWasm has no `msg.sender`-gated visibility and no implicit auth. Every `execute` branch is callable by anyone unless the handler checks `info.sender`.
ExecuteMsg::SetConfig { admin } => {
let mut cfg = CONFIG.load(deps.storage)?;
cfg.admin = admin; // ← no info.sender check
CONFIG.save(deps.storage, &cfg)?;
Ok(Response::new())
}**Signal:** an `ExecuteMsg` arm that mutates privileged state (`admin`, `owner`, `mint`, `withdraw`, `pause`) with no `if info.sender != cfg.admin { return Err(Unauthorized) }`. Anyone can dispatch any message.
let all: Vec<_> = BALANCES
.range(deps.storage, None, None, Order::Ascending) // ← whole map
.collect::<StdResult<_>>()?;
for (addr, bal) in all { /* mutate each */ }**Signal:** `.range(.., None, None, ..)` / `.keys(..)` over a user-growable `Map` inside `execute`, or a loop whose length attackers control. Each entry costs gas; an attacker inflates the map until the handler always runs out of gas (permanent freeze). Paginate with `start_after` + `limit`.
#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
let res = msg.result.unwrap(); // ← assumes success; panics on err
// no match on msg.id → all replies handled identically
parse_instantiate_response_data(&res.data.unwrap())?;
Ok(Response::new())
}**Signal:** a `reply` handler that doesn't `match msg.id { ... }` against known reply IDs, or `.unwrap()`s `msg.result`/`msg.result.data`, or trusts `SubMsgResult` data without checking it came from the expected submessage. Different submessages can land in the same reply with attacker-influenced data.
#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
CONFIG.save(deps.storage, &Config { admin: msg.new_admin })?; // ← arbitrary rewrite
Ok(Response::new()) // no version/sender guard
}**Signal:** `migrate` that overwrites owner/config from `MigrateMsg` without checking `set_contract_version`/`get_contract_version` (downgrade or replay), or a centralization risk where the code-admin can swap logic+state at will. Even with chain-level migrate auth, document who holds the admin.
let recipient = msg.to; // ← String, never validated BALANCES.save(deps.storage, &Addr::unchecked(&recipient), &amt)?; // ← unchecked
**Signal:** `Addr::unchecked(...)` on user input, or storing/using a `String` address without `deps.api.addr_validate(&s)?`. Unvalidated/un-normalized addresses split balances across casing/bech32 variants and break invariants.
ExecuteMsg::Deposit {} => {
let amount = msg.amount; // ← trusts a field, not info.funds
CREDIT.save(deps.storage, &info.sender, &amount)?;
Ok(Response::new())
}**Signal:** crediting a deposit from a message field instead of `info.funds`, or not asserting `info.funds` contains the expected denom AND amount (`must_pay`/`one_coin` from `cw_utils`). Also flag handlers that ignore unexpected attached funds.
let total = a + b; // ← Uint128 Add can overflow → error, let scaled = price * qty; // but `as`/u128 casts wrap silently
**Signal:** `Uint128`/`Uint256` `+`/`-`/`*` where overflow should be a domain error vs a panic, OR casting to/from primitive `u128`/`u64` with `as`/`.u128()` in accounting. Prefer `checked_add`/`checked_sub`/`checked_mul` returning `OverflowError`.
let price: PriceResp = deps.querier.query_wasm_smart(oracle, &QueryMsg::Price {})?;**Signal:** acting on a `query_wasm_smart` result from a user-supplied contract address, or assuming queries are side-effect-free guarantees (a malicious queried contract returns adversarial data). Validate the oracle/contract address against an allowlist.
| Pattern | Severity | Notes | |---|---|---| | Execute arm mutating privileged state, no `info.sender` check | **Critical** | Anyone seizes admin/funds | | Unbounded `.range(None,None)` over user-growable map | **High** | Permanent gas-DoS freeze | | `reply` without `match msg.id` / unwrap of result | **High** | Cross-submsg data confusion | | `migrate` overwrites owner/config unguarded | **High** | Logic+state backdoor | | Deposit credited from field, not `info.funds` | **High** | Free credit / fund theft | | `Uint128` overflow / lossy primitive cast | **High** | Value-dependent | | `Addr::unchecked` on user input | **Medium** | A
Rugproof your code before someone else does. 🌐 Live site: omermaksutii.github.io/RugProof 📦 Latest: v1.0.0 — 45 commands · 23 agents · 45 skills · 13 MCP servers · tested, offline-first, with rule packs, a benchmark, non-EVM coverage, and post-deploy
Repo: omermaksutii/RugProof
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove deliberately skip the EXTCODESIZE…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta,…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the hook's permissions live in the…