Thunder Loan

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

Upgrade to ThunderLoanUpgraded causes storage collision, corrupting the flashloan fee from 0.3% to 100%

Root + Impact

Description:

When a UUPS proxy is upgraded, the new implementation must maintain the exact same storage layout as the old one. In the original ThunderLoan contract, the variables are declared in order: s_tokenToAssetToken, s_feePrecision, and s_flashLoanFee. The upgraded contract removes the s_feePrecision variable and replaces it with a compile-time constant (FEE_PRECISION). Because Solidity constants do not occupy storage slots, every variable declared after it shifts up by one slot. Consequently, the new s_flashLoanFee variable now points to the storage slot previously occupied by s_feePrecision. Upon upgrade, it reads the old s_feePrecision value (1e18) as the fee, corrupting the fee from 0.3% (3e15) to 100% (1e18) and making all flashloans prohibitively expensive.

// src/upgradedProtocol/ThunderLoanUpgraded.sol
mapping(IERC20 => AssetToken) public s_tokenToAssetToken;
@> // Removed: uint256 private s_feePrecision; (shifts subsequent slots)
@> uint256 private s_flashLoanFee; // Now reads 1e18
uint256 public constant FEE_PRECISION = 1e18;

Risk

Likelihood: High. The storage layout shift is deterministic; it will occur automatically the moment the upgrade is executed on any deployed instance.

Impact: High. The flashloan fee is corrupted to 100%, making the protocol unusable. The corruption happens silently without emitting an event, requiring manual owner intervention to reset the fee.

Proof of Concept:

The PoC upgrades the proxy and asserts that the fee changes from 0.3% (3e15) to 100% (1e18).

contract PoC is Test {
function testExploit() public {
assertEq(tl.getFee(), 3e15, "Pre-upgrade fee");
tl.upgradeTo(address(implUpgraded));
assertEq(ThunderLoanUpgraded(address(proxy)).getFee(), 1e18, "Post-upgrade corrupted fee");
}
}

Recommended Mitigation:

Preserve the storage layout by leaving a placeholder variable where the old one existed.

mapping(IERC20 => AssetToken) public s_tokenToAssetToken;
+ uint256 private __deprecated_s_feePrecision;
uint256 private s_flashLoanFee;
uint256 public constant FEE_PRECISION = 1e18;
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!