Skip to content
Security
Skill

/solady-ownable-init-frontrun

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

From plugin
rugproof
952 skills23 agents45 commands4 hooks
Install
$ npx -y skills add omermaksutii/RugProof --skill solady-ownable-init-frontrun --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-ownable-init-frontrun

Context 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

SKILL.md

solady-ownable-init-frontrun.SKILL.md
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.

Solady Ownable initializer front-run detection

When this applies

Trigger on any of:

  • `import {Ownable} from "solady/auth/Ownable.sol";` or `OwnableRoles`
  • A contract calling `_initializeOwner(...)` from a public/external `initialize()` rather than the constructor
  • Minimal-proxy clones (`LibClone.clone` / EIP-1167) of an Ownable implementation
  • Factory deployments where `create`/`create2` and `initialize()` are two separate transactions
  • Implementation contracts behind proxies (UUPS / transparent) using Solady Ownable
  • Any `initialize`/`init` that is not protected by an initializer guard or atomic deploy

Detection patterns

Public init, non-atomic deploy (HIGH)

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 mempool

Between `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.

Implementation left uninitialized (HIGH)

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.

Re-init via `_setOwner` exposure (MEDIUM-HIGH)

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.

Severity rubric

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

Remediation patterns

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

False-positive notes

  • Constructor-based `_initializeOwner` (non-clone deploy) is not front-runnable — Info.
  • Factory that deploys and initializes in one call (atomic) — not exploitable.
  • Initializer gated on `msg.sender == factory` or consuming clone immutable args — safe.

Related

  • [[access-control]]
  • [[delegatecall-risks]] — UUPS implementation hijack
  • [[centralization-risk]]
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.