Weather Witness

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

[H-2] Reentrancy Vulnerability in `WeatherNft::requestMintWeatherNFT` function.

Root + Impact

Reentrancy Vulnerability in WeatherNft::requestMintWeatherNFT function.

Impact: High severity—could allow unauthorized minting or fund loss if exploited.

Description

The WeatherNft.sol::requestMintWeatherNFT function enables users to mint NFTs tied to geographic locations by paying ETH and triggering a Chainlink Functions request. The function calls _sendFunctionsWeatherFetchRequest, an external interaction likely with Chainlink, but lacks reentrancy protection. This allows an attacker to re-enter the function before its state updates complete, potentially minting multiple NFTs or manipulating state.

  • Describe the normal behavior in one or more sentences

The function normally accepts ETH for minting, sends a weather data request via Chainlink Functions, and stores the request details for later fulfillment, increasing the mint price with each call.

  • Explain the specific issue or problem in one or more sentences

The issue arises because the external call to _sendFunctionsWeatherFetchRequest could be intercepted by a malicious contract, enabling reentrancy. Without protection, an attacker could re-trigger the function, incrementing s_currentMintPrice multiple times or minting duplicate NFTs.

function requestMintWeatherNFT(
string memory _pincode,
string memory _isoCode,
bool _registerKeeper,
uint256 _heartbeat,
uint256 _initLinkDeposit
) external payable returns (bytes32 _reqId) {
//@> Start of root cause
require(
msg.value == s_currentMintPrice,
WeatherNft__InvalidAmountSent()
);
s_currentMintPrice += s_stepIncreasePerMint;
//@> End of root cause - Potential state change before external call
if (_registerKeeper) {
IERC20(s_link).safeTransferFrom(
msg.sender,
address(this),
_initLinkDeposit
);
}
//@> Start of root cause - External call vulnerable to reentrancy
_reqId = _sendFunctionsWeatherFetchRequest(_pincode, _isoCode);
//@> End of root cause
emit WeatherNFTMintRequestSent(msg.sender, _pincode, _isoCode, _reqId);
s_funcReqIdToUserMintReq[_reqId] = UserMintRequest({
user: msg.sender,
pincode: _pincode,
isoCode: _isoCode,
registerKeeper: _registerKeeper,
heartbeat: _heartbeat,
initLinkDeposit: _initLinkDeposit
});
}
## Risk
**Likelihood**:
An attacker deploys a malicious contract mimicking Chainlinks response, re-entering requestMintWeatherNFT during _sendFunctionsWeatherFetchRequest.
The lack of reentrancy protection allows repeated calls before state updates (e.g., s_currentMintPrice) are finalized.
**Impact**:
Attackers could mint multiple NFTs with a single ETH payment, draining contract funds or LINK deposits.
Manipulation of s_currentMintPrice could disrupt the minting economy, affecting all users.
## Proof of Concept
```solidity
contract Attack {
WeatherNft public weatherNft;
uint256 public attackCount;
constructor(address _weatherNft) {
weatherNft = WeatherNft(_weatherNft);
}
function attack() external payable {
weatherNft.requestMintWeatherNFT{value: 0.1 ether}(
"12345", // pincode
"US", // isoCode
false, // _registerKeeper
0, // _heartbeat
0 // _initLinkDeposit
);
}
// Simulate Chainlink callback to re-enter
function _sendFunctionsWeatherFetchRequest(string memory, string memory) external returns (bytes32) {
if (attackCount < 5) {
attackCount++;
weatherNft.requestMintWeatherNFT{value: 0}("12345", "US", false, 0, 0);
}
return keccak256(abi.encodePacked(block.timestamp));
}
}
  1. Attacker scenario: Reentrancy exploit via malicious Chainlink callback.

  2. Simulate Chainlink callback to re-enter

Recommended Mitigation

Add reentrancy protection to prevent multiple entries during external calls.

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract WeatherNft is ReentrancyGuard {
// ... other code ...
function requestMintWeatherNFT(
string memory _pincode,
string memory _isoCode,
bool _registerKeeper,
uint256 _heartbeat,
uint256 _initLinkDeposit
) external payable nonReentrant returns (bytes32 _reqId) {
require(
msg.value == s_currentMintPrice,
WeatherNft__InvalidAmountSent()
);
s_currentMintPrice += s_stepIncreasePerMint;
if (_registerKeeper) {
IERC20(s_link).safeTransferFrom(
msg.sender,
address(this),
_initLinkDeposit
);
}
_reqId = _sendFunctionsWeatherFetchRequest(_pincode, _isoCode);
emit WeatherNFTMintRequestSent(msg.sender, _pincode, _isoCode, _reqId);
s_funcReqIdToUserMintReq[_reqId] = UserMintRequest({
user: msg.sender,
pincode: _pincode,
isoCode: _isoCode,
registerKeeper: _registerKeeper,
heartbeat: _heartbeat,
initLinkDeposit: _initLinkDeposit
});
}
}

Alternatively, ensure _sendFunctionsWeatherFetchRequest is internal or uses a reentrancy guard within its implementation.

Updates

Appeal created

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

Support

FAQs

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