Weather Witness

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

User can fulfill one requests multiple times

Root + Impact

Description

  • The contract allows the same user (or any user) to call fulfillMintRequest multiple times with the same requestId, resulting in multiple NFTs being minted for a single weather request.

function fulfillMintRequest(bytes32 requestId) external {
bytes memory response = s_funcReqIdToMintFunctionReqResponse[requestId].response;
bytes memory err = s_funcReqIdToMintFunctionReqResponse[requestId].err;
require(response.length > 0 || err.length > 0, WeatherNft__Unauthorized());
// @> No check to prevent multiple fulfillment for the same requestId
// @> s\_isRequestFulfilled\[requestId] is not checked/set before minting
}

Risk

Likelihood:

  • This will occur whenever a user (or attacker) calls fulfillMintRequest more than once with the same requestId, as there is no restriction in place to prevent repeated minting for the same request.

  • Automated bots or malicious users can exploit this by monitoring events and repeatedly calling the function.

Impact:

  • Multiple NFTs can be minted for a single weather request, breaking NFT uniqueness.

  • Users or attackers can obtain more NFTs than they paid for, leading to economic loss and loss of trust in the platform.

Proof of Concept

This test should be passing to confirm it is not allowed (revert is expected on second function call:

function testMintRequestCanOnlyBeFulfilledOnce() public {
// Setup
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = false;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
uint256 tokenId = weatherNft.s_tokenCounter();
// User requests mint
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{value: weatherNft.s_currentMintPrice()}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
// Get requestId from logs
Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 reqId;
for (uint256 i; i < logs.length; i++) {
if (logs[i].topics[0] == keccak256("WeatherNFTMintRequestSent(address,string,string,bytes32)")) {
(, , , reqId) = abi.decode(logs[i].data, (address, string, string, bytes32));
break;
}
}
assert(reqId != bytes32(0));
// User claims NFT
vm.prank(user);
weatherNft.fulfillMintRequest(reqId);
// Assert user owns the NFT
assertEq(weatherNft.ownerOf(tokenId), user);
vm.expectRevert();
vm.prank(user);
weatherNft.fulfillMintRequest(reqId);
}

Recommended Mitigation

  • Add a fulfillment tracking mapping and require statement to prevent multiple mints per request:

+ mapping(bytes32 => bool) public s_isRequestFulfilled;
//existing code
function fulfillMintRequest(bytes32 requestId) external {
+ require(!s_isRequestFulfilled[requestId], "Already fulfilled");
+ s_isRequestFulfilled[requestId] = true;
Updates

Appeal created

bube Lead Judge 23 days 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.