Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Impact: high
Likelihood: low
Invalid

# `initialize()` is unprotected and left uncalled by the deploy script, letting an attacker front-run it to seize ownership

initialize() is unprotected and left uncalled by the deploy script, letting an attacker front-run it to seize ownership

Severity: Low · Impact: High · Likelihood: Low

Description

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

// script/DeployThunderLoan.s.sol
ThunderLoan thunderLoan = new ThunderLoan();
@> new ERC1967Proxy(address(thunderLoan), ""); // empty init data; initialize() never called
// ThunderLoan.initialize sets the caller as owner:
function initialize(address tswapAddress) external initializer {
@> __Ownable_init(); // owner = msg.sender (whoever calls first)
...
}

Risk

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.

Proof of Concept

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.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { Test } from "forge-std/Test.sol";
import { ThunderLoan } from "../src/protocol/ThunderLoan.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract PocL1 is Test {
function test_L1_initializer_frontrun() public {
ThunderLoan impl = new ThunderLoan();
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); // as in the deploy script
ThunderLoan tl = ThunderLoan(address(proxy));
address attacker = makeAddr("attacker");
vm.prank(attacker);
tl.initialize(makeAddr("attackerOracle")); // first caller wins
assertEq(tl.owner(), attacker); // attacker owns the protocol and can upgrade it
}
}

Recommended Mitigation

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.

- new ERC1967Proxy(address(thunderLoan), "");
+ bytes memory initData = abi.encodeCall(ThunderLoan.initialize, (tswapAddress));
+ new ERC1967Proxy(address(thunderLoan), initData);
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!