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 front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with `AlreadyInitialized` on a second call) but it is NOT access-controlled, so in constructor-less deployment paths
$ npx -y skills add omermaksutii/RugProof --skill solady-ownable-init-frontrun --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/solady-ownable-init-frontrunContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with `AlreadyInitialized` on a second call) but it is NOT access-controlled, so in constructor-less deployment paths
name: solady-ownable-init-frontrun description: Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with `AlreadyInitialized` on a second call) but it is NOT access-controlled, so in constructor-less deployment paths (minimal-proxy clones, EIP-1167, factory `create`/`create2` without atomic init) an attacker can call the public initializer first and seize ownership. Activate on solady Ownable/OwnableRoles in clones, factories, or any non-atomic deploy+init.
Trigger on any of:
contract Vault is Ownable {
function initialize(address owner) external {
_initializeOwner(owner); // reverts on 2nd call — but ANYONE can make the 1st
}
}
// Factory:
address v = LibClone.clone(impl);
Vault(v).initialize(msg.sender); // ← separate tx: front-runnable in the mempoolBetween `clone` and `initialize`, a searcher front-runs `initialize(attacker)`. `_initializeOwner` succeeds for them; the legit call then reverts `AlreadyInitialized`. **Signal:** `_initializeOwner` reachable from an unguarded external function and deploy/init are not in one transaction.
contract Impl is Ownable {
function initialize(address o) external { _initializeOwner(o); }
}
// Impl deployed standalone, never initialized → anyone claims it.For UUPS, an attacker who owns the *implementation* can call `upgradeTo`/`selfdestruct`-style logic and brick or hijack all proxies pointing at it. **Signal:** Solady Ownable implementation deployed but not initialized in the same tx, and the implementation itself is callable.
function _setOwner(address o) internal { ... } // Solady internal
function rescueOwner(address o) external { _setOwner(o); } // ← bypasses init guard entirely`_setOwner` has no `AlreadyInitialized` guard; exposing it publicly defeats the one-time protection. **Signal:** `_setOwner` wrapped in an unprotected external function.
| Pattern | Severity | Notes | |---|---|---| | Clone/factory with non-atomic public initialize | **High** | Ownership theft, mempool front-run | | Uninitialized implementation behind proxy | **High** | Impl hijack → proxy compromise | | `_setOwner` exposed externally | **High** | Init guard bypassed entirely | | Init gated to factory `msg.sender` / atomic deploy | **Info** | Correctly protected |
1. **Atomic deploy+init** — initialize inside the same transaction as `clone`/`create`, or use `LibClone.cloneDeterministic` + immediate init in the factory call. 2. **Restrict the initializer** — `require(msg.sender == factory)` or pass owner via clone immutable args (`LibClone.clone(impl, immutableArgs)`). 3. **Lock the implementation** — call `_initializeOwner(deadAddress)` / `_disableInitializers`-equivalent in the implementation's constructor so the standalone impl can't be claimed. 4. **Never expose `_setOwner`** — only `transferOwnership` (owner-gated) and the guarded `_initializeOwner`.
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 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…