Thunder Loan

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

setAllowedToken(token, false) deletes the AssetToken mapping while deposits remain, permanently locking liquidity providers' funds

Root + Impact

Description

setAllowedToken(token, false) removes a token from the protocol by deleting its AssetToken mapping entry, with no check that deposits still exist:

function setAllowedToken(IERC20 token, bool allowed) external onlyOwner returns (AssetToken) {
if (allowed) {
...
} else {
AssetToken assetToken = s_tokenToAssetToken[token];
@> delete s_tokenToAssetToken[token]; // wipes the mapping even if deposits remain
emit AllowedTokenSet(token, assetToken, allowed);
return assetToken;
}
}

Liquidity providers' underlying lives inside the AssetToken, and the only way to retrieve it is redeem(), which is gated by revertIfNotAllowedToken. Once the token is disallowed, isAllowedToken returns false, so every redeem() reverts with ThunderLoan__NotAllowedToken. The deposited funds are permanently locked - there is no other withdrawal path.

Risk

Likelihood: Low - requires the owner to disallow a token that still holds deposits. The owner is trusted, but this is an easy, irreversible foot-gun with no on-chain guard.

Impact: High - all liquidity providers in that token permanently lose access to their deposited underlying; the funds become unrecoverable.

Proof of Concept

An LP deposits, the owner disallows the token, and the LP can no longer redeem. Runnable Foundry test (extend BaseTest):

function test_PoC_setAllowedTokenFalseLocksDeposits() public {
thunderLoan.setAllowedToken(tokenA, true);
address lp = makeAddr("lp");
uint256 amount = 100e18;
tokenA.mint(lp, amount);
vm.startPrank(lp);
tokenA.approve(address(thunderLoan), amount);
thunderLoan.deposit(tokenA, amount);
vm.stopPrank();
// owner disallows the token while the LP's deposit is still inside the AssetToken
thunderLoan.setAllowedToken(tokenA, false);
// the LP can no longer redeem: redeem reverts with ThunderLoan__NotAllowedToken
vm.startPrank(lp);
vm.expectRevert();
thunderLoan.redeem(tokenA, 1e18);
vm.stopPrank();
}

Run forge test --mt test_PoC_setAllowedTokenFalseLocksDeposits -vv; it passes, showing redemption is permanently blocked.

Recommended Mitigation

Block disallowing a token that still has outstanding deposits, or provide an emergency redemption path that does not depend on the allowed-token check:

} else {
AssetToken assetToken = s_tokenToAssetToken[token];
+ if (assetToken.totalSupply() != 0) {
+ revert ThunderLoan__CantBeZero(); // or a dedicated "still has deposits" error
+ }
delete s_tokenToAssetToken[token];
emit AllowedTokenSet(token, assetToken, allowed);
return assetToken;
}
Updates

Lead Judging Commences

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