Skip to content
Security
Skill

/solady-erc20-permit2-assumptions

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

From plugin
rugproof
952 skills23 agents45 commands4 hooks
Install
$ npx -y skills add omermaksutii/RugProof --skill solady-erc20-permit2-assumptions --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/solady-erc20-permit2-assumptions

Context 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

SKILL.md

solady-erc20-permit2-assumptions.SKILL.md
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.

Solady ERC20 / permit / DN404 assumption detection

When this applies

Trigger on any of:

  • `import {ERC20} from "solady/tokens/ERC20.sol";` or `ERC2612`, `DN404`, `DN404Mirror`, `ERC4626` from solady
  • Integrations calling `permit(...)` and assuming a fixed EIP-712 domain / version string
  • Code relying on Solady's revert *reasons* (Solady reverts with custom errors / 4-byte selectors, not strings)
  • DN404 tokens treated as plain ERC20 (the NFT mirror has separate transfer semantics)
  • Off-chain indexers decoding events / revert data assuming OZ layout
  • Overriding `name()`, `symbol()`, `_constantNameHash`, `_versionHash`, or `_domainNameAndVersion`

Detection patterns

Hardcoded permit domain separator (MEDIUM)

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 differ

Solady 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.

Relying on revert strings (MEDIUM)

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.

DN404 treated as vanilla ERC20 (MEDIUM)

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.

Assuming metadata always present (LOW-MEDIUM)

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.

Severity rubric

| 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 |

Remediation patterns

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.

False-positive notes

  • Reading `DOMAIN_SEPARATOR()` directly (not reconstructing it) is correct — no finding.
  • A non-DN404 Solady ERC20 with metadata overridden is a normal token — only flag missing overrides.
  • Generic `catch {}` already handles custom errors — downgrade.

Related

  • [[signature-malleability]] — permit signature handling
  • [[token-compatibility]] — non-standard return / metadata
  • [[reentrancy]] — DN404 mirror callbacks
Read more
Ships withrugproof

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

Get the whole plugin

Other skills on rugproof.