Weather Witness

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

Missing Access Control in fulfillMintRequest Allows NFT Theft

Description

  • The WeatherNft contract allows users to request new NFTs by calling requestMintWeatherNFT, which initiates a Chainlink Functions request to fetch weather data.

  • However, the fulfillMintRequest function that completes the minting process has no access control to ensure that only the original requester can mint their NFT.

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 verification that msg.sender is the original requester
if (response.length == 0 || err.length > 0) {
return;
}
UserMintRequest memory _userMintRequest = s_funcReqIdToUserMintReq[
requestId
];
// ...
_mint(msg.sender, tokenId); // Mints to the caller, not the original requester
// ...
}

Risk

Likelihood: High

  • The vulnerability is immediately exploitable once a Chainlink Functions response is available

  • Any transaction monitoring the mempool can frontrun legitimate users' fulfillment calls

  • No technical barriers prevent exploitation

Impact: High

  • Attackers can steal all NFTs by monitoring for Chainlink Functions responses and calling fulfillMintRequest before legitimate users

  • Users lose the ETH paid for minting (s_currentMintPrice) plus any LINK tokens provided for keeper registration

  • Malicious actors can gain ownership of NFTs they didn't pay for, along with their associated Chainlink Automation upkeeps

Proof of Concept

// 1. Victim calls requestMintWeatherNFT and pays ETH + LINK
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());
// ... store request data with victim's address ...
}
// 2. Chainlink Functions provides result via fulfillRequest
// 3. Attacker monitors for new responses and calls fulfillMintRequest before victim
function fulfillMintRequest(bytes32 requestId) external {
// ... validate response exists ...
// 4. NFT is minted to attacker (msg.sender) instead of original requester
_mint(msg.sender, tokenId);
}

Recommended Mitigation

Ensure only the original requester can fulfill their mint 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());
+ UserMintRequest memory _userMintRequest = s_funcReqIdToUserMintReq[requestId];
+ require(msg.sender == _userMintRequest.user, "WeatherNft: caller is not the requester");
if (response.length == 0 || err.length > 0) {
return;
}
- UserMintRequest memory _userMintRequest = s_funcReqIdToUserMintReq[requestId];
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
emit WeatherNFTMinted(
requestId,
- msg.sender,
+ _userMintRequest.user,
Weather(weather)
);
- _mint(msg.sender, tokenId);
+ _mint(_userMintRequest.user, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);
// ... rest of the function ...
}

Alternatively, implement an automatic fulfillment mechanism that doesn't require users to call a separate function.

Updates

Appeal created

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

Lack of ownership check in `fulfillMintRequest` function

There is no check to ensure that the caller of the `fulfillMintRequest` function is actually the owner of the `requestId`. This allows a malicious user to receive a NFT that is payed from someone else.

Support

FAQs

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