Thunder Loan

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

`ThunderLoan.initialize` accepts a zero address for `tswapAddress`, silently misconfiguring the oracle pool factory with no way to recover (initializer is single-use)

Root + Impact

Description

  • ThunderLoan.initialize(address tswapAddress) is the proxy's one-shot initializer (protected by OpenZeppelin's initializer modifier). It forwards tswapAddress to __Oracle_init, which stores it as s_poolFactory in OracleUpgradeable.

  • Neither initialize nor __Oracle_init_unchained validates that tswapAddress is non-zero. If the deployer accidentally passes address(0) (script bug, copy-paste error, missing environment variable, etc.), the contract is initialized with no oracle. Every subsequent call to getPriceInWeth reverts (calling getPool on address(0)), breaking getCalculatedFee and therefore both deposit and flashloan. Because initialize is initializer, it cannot be called again to fix the misconfiguration — the proxy is permanently bricked.

// src/protocol/ThunderLoan.sol
function initialize(address tswapAddress) external initializer {
__Ownable_init();
__UUPSUpgradeable_init();
@> __Oracle_init(tswapAddress); // no zero check
s_feePrecision = 1e18;
s_flashLoanFee = 3e15;
}
// src/protocol/OracleUpgradeable.sol
function __Oracle_init_unchained(address poolFactoryAddress) internal onlyInitializing {
@> s_poolFactory = poolFactoryAddress; // accepts address(0)
}

Risk

Likelihood:

  • Requires the deployer (admin / trusted) to mistakenly pass address(0). Possible via script error, missing config var, or copy-paste from a template.

  • Detection is on the first user-facing call (deposit / flashloan), not at deploy time — the misconfiguration is silent.
    Impact:

  • The proxy is unrecoverable. initializer prevents re-running initialize; there is no setter for s_poolFactory. The only recovery is an upgrade to a new implementation that adds a setter, which depends on _authorizeUpgrade and the owner's diligence.

  • Any tokens already deposited before the first oracle-touching call are stuck (they cannot be withdrawn either, because redeem does not need the oracle — actually they can be withdrawn; the impact is the protocol cannot service flash loans or take new deposits, but existing LP funds remain accessible via redeem).

Proof of Concept

function test_BUG6_initializeAcceptsZeroPoolFactory() public {
ThunderLoan freshImpl = new ThunderLoan();
ERC1967Proxy freshProxy = new ERC1967Proxy(address(freshImpl), "");
ThunderLoan freshTL = ThunderLoan(address(freshProxy));
freshTL.initialize(address(0)); // accepted silently
assertEq(freshTL.getPoolFactoryAddress(), address(0));
// initialize() cannot be called again; oracle is permanently misconfigured.
}

Test passes — initialization with address(0) succeeds.

Recommended Mitigation

Add a zero-address guard either in initialize or in __Oracle_init_unchained:

function initialize(address tswapAddress) external initializer {
+ if (tswapAddress == address(0)) revert ThunderLoan__CantBeZero();
__Ownable_init();
__UUPSUpgradeable_init();
__Oracle_init(tswapAddress);
s_feePrecision = 1e18;
s_flashLoanFee = 3e15;
}

Adding the check in __Oracle_init_unchained is also valid and protects any other contract that inherits the oracle:

function __Oracle_init_unchained(address poolFactoryAddress) internal onlyInitializing {
+ require(poolFactoryAddress != address(0), "Oracle: zero factory");
s_poolFactory = poolFactoryAddress;
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 16 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!