initialize() is unprotected and left uncalled by the deploy script, letting an attacker front-run it to seize ownershipSeverity: Low · Impact: High · Likelihood: Low
For a UUPS proxy, the implementation's initialize() sets the owner (via __Ownable_init) and the oracle, and must be called by the deployer in the same transaction as (or atomically with) the proxy deployment so no one else can call it first.
The deploy script creates the proxy with empty init data and never calls initialize(), so on-chain the proxy sits uninitialized and initialize() is callable by anyone.
Likelihood:
Occurs when an attacker observes the proxy deployment in the mempool and calls initialize() before the legitimate deployer does. Rated Low because a careful deployer can bundle/immediately re-deploy, but the window is real given the script as written.
Impact:
The attacker becomes owner, which authorizes UUPS upgrades (_authorizeUpgrade is onlyOwner) — they can upgrade to a malicious implementation and drain the entire protocol, and can set an attacker-controlled oracle. Full compromise.
Save the block below as test/PocL1.t.sol and run forge test --mt test_L1_initializer_frontrun. An arbitrary account initializes the proxy and becomes owner.
Initialize atomically with deployment so the proxy can never exist in an uninitialized state — pass the encoded initialize call as the ERC1967Proxy constructor's _data.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.