Thunder Loan

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

Proxy is deployed with empty init data and initialize() is unprotected — anyone can front-run initialization to seize permanent ownership

Description

  • Normal behavior: A UUPS proxy should be initialized atomically (in the same transaction as deployment, via the proxy's constructor data) so only the legitimate deployer can ever become owner.

  • Issue: script/DeployThunderLoan.s.sol deploys the proxy with empty init data (new ERC1967Proxy(address(thunderLoan), "")) and never calls initialize(). The proxy is left uninitialized, and initialize() has no caller restriction beyond the one-time initializer modifier. Any external account can call initialize() first; __Ownable_init() then sets that caller as owner.

// script/DeployThunderLoan.s.sol
new ERC1967Proxy(address(thunderLoan), ""); // @> empty init data, initialize() never called here
​
// ThunderLoan.sol
function initialize(address tswapAddress) external initializer { // @> no access control
__Ownable_init(); // sets owner = msg.sender (attacker)
...
}

Risk

Likelihood: Medium

  • The proxy is deployed uninitialized; anyone monitoring the chain can submit initialize() before the deployer’s follow-up transaction is mined.

Impact: High

  • The attacker becomes the permanent owner, controlling _authorizeUpgrade (can upgrade to a malicious implementation and drain all funds), setAllowedToken, and updateFlashLoanFee — full protocol takeover.

Proof of Concept

Run forge test --match-contract PoC_InitializerFrontrun -vv:

import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
​
function test_AnyoneCanFrontrunInitialize_TakeOwnership() public {
ThunderLoan impl = new ThunderLoan();
ERC1967Proxy p = new ERC1967Proxy(address(impl), ""); // deployed like the script: empty init
ThunderLoan tl = ThunderLoan(address(p));
address attacker = address(0xBAD);
vm.prank(attacker);
tl.initialize(address(mockPoolFactory)); // attacker front-runs initialization
assertEq(tl.owner(), attacker); // attacker now owns the proxy
}

Result: tl.owner() == attacker.

Recommended Mitigation

Initialize atomically in the proxy constructor so initialization cannot be front-run:

- 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 1 hour 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!