Weather Witness

First Flight #40
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

Reentrancy via Malicious ERC20 (LINK) Transfer

Root: The contract performs an external token transfer call before updating internal state or protecting reentrancy.

Impact: A malicious token contract can re-enter the function before the state is consistent, leading to duplicated minting or state manipulation.

Description

  • Normally, the function requestMintWeatherNFT registers a keeper and collects LINK tokens from the user using safeTransferFrom, then proceeds with other logic including requesting weather data and storing the mint request.

  • However, if a malicious ERC20 token contract is used, it can reenter the same function or related logic during the safeTransferFrom call, because state updates or locks (like nonReentrant) are not applied yet.

if (_registerKeeper) {
@> IERC20(s_link).safeTransferFrom(
@> msg.sender,
@> address(this),
@> _initLinkDeposit
@> );
}

CopyEdit

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Attack surface opens when malicious tokens are used or in untrusted deployments.

  • Commonly missed in test environments using mock ERC20s.

Impact:

  • Double minting, corrupted accounting, or stuck LINK.

Proof of Concept: The MaliciousLINK token exploits the ERC20 transfer hook to re-enter the original requestMintWeatherNFT call during the safeTransferFrom, allowing attackers to bypass mint price increases, limits, or even mint multiple NFTs in one transaction.

contract MaliciousLINK {
function transferFrom(...) external returns (bool) {
weatherNft.requestMintWeatherNFT(...); // reenters before original finishes
return true;
}
}

Recommended Mitigation - This mitigation ensures that state changes occur before any external calls. Alternatively, applying a nonReentrant modifier to the function blocks recursive calls entirely. This defends against reentrancy by ensuring either:
(1) all internal state is locked before any external interaction, or
(2) the function can't be re-entered at all.

+ uint256 localMintPrice = s_currentMintPrice;
+ s_currentMintPrice += s_stepIncreasePerMint;
+ s_funcReqIdToUserMintReq[tempReqId] = UserMintRequest({...});
- IERC20(s_link).safeTransferFrom(...);
+ Use `nonReentrant` modifier or move transfer after state update.
Updates

Appeal created

bube Lead Judge 23 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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