Weather Witness

First Flight #40
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Lack of Replay Protection in `fulfillMintRequest` Allows Duplicate NFT Minting

Root + Impact

Description

  • Normal Behavior: Each Chainlink request ID should correspond to a single NFT mint. Once fulfillMintRequest processes a request, further calls with the same ID should be rejected.

  • Issue: The contract does not clear or invalidate the UserMintRequest after successful fulfillment, allowing anyone (including the original user) to call fulfillMintRequest multiple times for the same requestId and mint duplicate NFTs.

function fulfillMintRequest(bytes32 requestId) external {
UserMintRequest memory _userMintRequest = s_funcReqIdToUserMintReq[requestId];
// …
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
_mint(msg.sender, tokenId); // @> Mapping for requestId remains after mint
s_tokenIdToWeather[tokenId] = Weather(weather);
// …
}

Risk

Likelihood:

  • Bots or users can easily replay on‑chain transactions once the oracle’s response is stored.

  • No on‑chain mechanism exists to automatically expire or consume a requestId.

Impact:

  • Multiple NFTs can be minted from a single paid request, diluting token scarcity and disrupting economic modeling.

  • Unexpected token ID collisions or inconsistencies in weather state tracking when multiple tokens share the same underlying data.

Proof of Concept

  1. A user pays to mint a Weather NFT, which generates a unique requestId (e.g. 0xABC...) and the oracle responds with the weather data.

  2. Anyone—whether it’s the same user or an attacker—can call fulfillMintRequest(0xABC...) once to mint the first NFT (say, token #100).

  3. Because the contract never clears the record of that requestId, calling fulfillMintRequest(0xABC...) a second time still succeeds, minting a second NFT (token #101) without any additional payment.

  4. This process can be repeated indefinitely, producing an arbitrary number of “unique” NFTs from a single paid request—breaking the one‑of‑a‑kind guarantee.

Recommended Mitigation

function fulfillMintRequest(bytes32 requestId) external {
+ UserMintRequest memory req = s_funcReqIdToUserMintReq[requestId];
+ require(req.user != address(0), "Request already fulfilled or invalid");
bytes memory response = s_funcReqIdToMintFunctionReqResponse[requestId].response;
bytes memory err = s_funcReqIdToMintFunctionReqResponse[requestId].err;
require(response.length > 0 || err.length > 0, WeatherNft__Unauthorized());
if (response.length == 0 || err.length > 0) {
return;
}
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
_mint(msg.sender, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);
+ // Invalidate this request to prevent replay
+ delete s_funcReqIdToUserMintReq[requestId];
}

By clearing the s_funcReqIdToUserMintReq entry at the end of fulfillMintRequest, you ensure that each request ID can only be used once, preventing duplicate NFT minting.

Updates

Appeal created

bube Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Multiple tokens for one `requestId`

The `WeatherNFT::fulfillMintRequest` allows a malicious user to call multiple times the function with the same `requestId`.

Support

FAQs

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