Thunder Loan

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

Storage slot collision in ThunderLoanUpgraded corrupts s_flashLoanFee from 0.3% to 100% upon UUPS upgrade

Summary

Upgrading the proxy from ThunderLoan to ThunderLoanUpgraded causes a critical storage layout collision. Because the state variable s_feePrecision was removed and replaced with a constant (FEE_PRECISION), all subsequent state variables in ThunderLoanUpgraded shift down by one storage slot, corrupting s_flashLoanFee from 3e15 (0.3%) to 1e18 (100%).

Vulnerability Details

In src/protocol/ThunderLoan.sol, the contract defines three sequential state variables after the inherited upgradeable base contracts:

uint256 private s_feePrecision; // Occupies Slot N (initialized to 1e18)
uint256 private s_flashLoanFee; // Occupies Slot N+1 (initialized to 3e15 = 0.3%)
mapping(IERC20 token => bool currentlyFlashLoaning) private s_currentlyFlashLoaning; // Occupies Slot N+2

In src/upgradedProtocol/ThunderLoanUpgraded.sol, s_feePrecision was replaced with uint256 public constant FEE_PRECISION = 1e18;. Because constant values are embedded directly in bytecode and do not occupy a storage slot, the storage layout in ThunderLoanUpgraded shifts:

uint256 public constant FEE_PRECISION = 1e18; // Does NOT occupy a storage slot!
uint256 private s_flashLoanFee; // Shifted into Slot N (reads old s_feePrecision = 1e18!)
mapping(IERC20 token => bool currentlyFlashLoaning) private s_currentlyFlashLoaning; // Shifted into Slot N+1!

Proof of Concept

Formal verification of upgrade storage-layout integrity (INV-10) on the ERC-1967 proxy proves that upgrading ThunderLoan to ThunderLoanUpgraded immediately corrupts getFee() without any state write:

You can also reproduce this in Foundry by adding the following test to test/unit/ThunderLoanTest.t.sol:

import { ThunderLoanUpgraded } from "../../src/upgradedProtocol/ThunderLoanUpgraded.sol";
​
function testUpgradeStorageCollisionCorruptsFee() public {
uint256 feeBefore = thunderLoan.getFee();
assertEq(feeBefore, 3e15);
​
ThunderLoanUpgraded upgradedImpl = new ThunderLoanUpgraded();
vm.prank(thunderLoan.owner());
thunderLoan.upgradeToAndCall(address(upgradedImpl), "");
​
uint256 feeAfter = thunderLoan.getFee();
// Fee is corrupted to 1e18 (old s_feePrecision slot) instead of 3e15!
assertEq(feeAfter, 1e18);
assertTrue(feeAfter != feeBefore);
}

Impact

Immediately upon upgrading to ThunderLoanUpgraded, s_flashLoanFee reads the value 1e18 (100% fee) stored in s_feePrecision's old slot, and s_currentlyFlashLoaning shifts into s_flashLoanFee's old slot, completely breaking flash loan fee calculations and protocol state integrity.

Tools Used

Manual Review, Foundry, Neuro-Symbolic Formal Verification (Z3).

Recommended Mitigation

Preserve the exact storage layout order in ThunderLoanUpgraded.sol by keeping s_feePrecision (or an unused storage placeholder variable) in its original storage slot before s_flashLoanFee:

--- a/src/upgradedProtocol/ThunderLoanUpgraded.sol
+++ b/src/upgradedProtocol/ThunderLoanUpgraded.sol
@@ -68,6 +68,7 @@ contract ThunderLoanUpgraded is Initializable, OwnableUpgradeable, UUPSUpgradeab
uint256 public constant FEE_PRECISION = 1e18;
+ uint256 private s_feePrecision; // Preserve storage slot alignment with ThunderLoan V1
uint256 private s_flashLoanFee; // 0.3% ETH fee
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!