The deployment script creates the ERC1967Proxy with empty initialization calldata ("") and never calls initialize in the same transaction. ThunderLoan.initialize has no caller restriction beyond the initializer modifier and sets owner = msg.sender (via __Ownable_init) and s_poolFactory = tswapAddress (via __Oracle_init). Between deployment and the operator's first initialize call, any address can call initialize first, becoming the owner and choosing the price factory. As owner, the attacker can immediately upgradeTo a malicious implementation (draining every AssetToken) and control the fee oracle.
DeployThunderLoan.run() deploys the implementation and the proxy but passes "" as the proxy's init data, so the proxy starts uninitialized. initialize (ThunderLoan.sol:139-145) is guarded only by initializer (one-shot, but first-caller-wins) — it does not verify the caller is the deployer. The intended atomic pattern is to pass abi.encodeCall(ThunderLoan.initialize, (tswap)) as the proxy constructor's data, so initialization happens in the same transaction as deployment. Because that is not done, the deployer's separate initialize call is front-runnable.
Likelihood: An attacker monitors the deployer's transactions, sees the proxy deployed without init data, and sends initialize(attackerFactory) first. Only the deployment window is exposed, but that window is created by the shipped script itself.
Impact: The attacker becomes owner and controls s_poolFactory. They can upgradeTo an implementation that calls assetToken.transferUnderlyingTo(attacker, balance) to drain all LP principal, and can make getPriceInWeth return arbitrary values. LPs and borrowers lose everything in the pool; the loss is total and unrecoverable.
The test deploys a fresh ThunderLoan implementation and a fresh ERC1967Proxy with empty data (mirroring the script), then pranks an attacker address and calls initialize(oracle). The assertion proves the attacker — not the deployer — is now owner() of the proxy.
Initialize atomically in the proxy constructor so no uninitialized window exists, and hand ownership to a multisig/timelock in the same transaction. This removes the front-run entirely because initialize cannot be called separately.
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.