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 unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via assembly, exposes a virtual `_constantNameHash`/`_versionHash` for permit domain separation, and its DN404 mirror splits
$ npx -y skills add omermaksutii/RugProof --skill solady-erc20-permit2-assumptions --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/solady-erc20-permit2-assumptionsContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via assembly, exposes a virtual `_constantNameHash`/`_versionHash` for permit domain separation, and its DN404 mirror splits
name: solady-erc20-permit2-assumptions description: Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via assembly, exposes a virtual `_constantNameHash`/`_versionHash` for permit domain separation, and its DN404 mirror splits ERC20/ERC721 logic — integrators that assume OZ-style behavior, revert strings, or that `name()`/`decimals()` are always present can misbehave. Activate on solady/tokens imports, ERC2612 permit flows, DN404, or off-chain code parsing Solady revert reasons.
Trigger on any of:
bytes32 DOMAIN = keccak256(abi.encode(
TYPE_HASH, keccak256("MyToken"), keccak256("1"), block.chainid, token
));
// ← assumes version "1"; Solady ERC2612 default version is "1" but DN404/overrides may differSolady derives the domain via `_domainNameAndVersion()` (default version `"1"`). If the token overrides `_versionHash` or `name()`, a hand-rolled separator mismatches and every `permit` reverts `InvalidPermit`. **Signal:** off-chain or on-chain code reconstructs the EIP-712 domain instead of reading `DOMAIN_SEPARATOR()` from the token.
try token.transferFrom(a, b, amt) returns (bool) {}
catch Error(string memory reason) { // ← Solady reverts with custom errors,
if (keccak256(bytes(reason)) == ...) {} // NOT Error(string); this branch never hits
}Solady reverts `InsufficientBalance()` / `InsufficientAllowance()` (4-byte). `catch Error(string)` won't match; only `catch (bytes memory)` / `catch` will. **Signal:** `catch Error(string)` or string comparison used to branch on a Solady token failure.
dn404Token.transfer(to, amt); // also mints/burns NFTs in the mirror; gas + reentrancy surface
DN404 fractionalizes an NFT collection; an ERC20 transfer can mint/burn mirror NFTs and invoke `onERC721Received`-style hooks. Treating it as inert ERC20 ignores added gas and a callback surface. **Signal:** DN404 token integrated through a path that assumes ERC20 transfers have no side effects / no callbacks.
uint8 d = IERC20(token).decimals(); // Solady ERC20 leaves name/symbol/decimals virtual
Solady's base ERC20 leaves `name()`/`symbol()` as `virtual` returning empty unless overridden; a minimal token may omit them. **Signal:** unguarded `decimals()`/`name()` read on a Solady-derived token.
| Pattern | Severity | Notes | |---|---|---| | Hand-rolled permit domain mismatching token → permits brick | **Medium** | Liveness, not loss; per-token | | Revert-string branching never executes | **Medium** | Silent wrong control flow | | DN404 used as plain ERC20 (callback / gas surprise) | **Medium** | Reentrancy/gas surface, see [[reentrancy]] | | Unguarded metadata read | **Low** | Reverts or returns empty |
1. **Read `DOMAIN_SEPARATOR()`** off the token instead of reconstructing it; never hardcode the version string. 2. **Catch `bytes`, not `Error(string)`** — match Solady custom-error selectors or use a generic `catch`. 3. **Detect DN404** (e.g. via `DN404Mirror` linkage / interface) and route it through NFT-aware logic; budget gas and treat transfers as callback-bearing. 4. **Guard metadata** with try/catch defaulting decimals to 18.
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 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…
Detect reentrancy in Uniswap V4 hooks via the PoolManager unlock/lock callback. V4 uses a singleton PoolManager with transient lock state; all pool mutations…