Weather Witness

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

NFT Can Be Stolen by Front-Running the fulfillMintRequest() Function

Root + Impact

Root: Lack of access control and incorrect recipient logic in fulfillMintRequest() it mints to the caller (msg.sender) instead of the original user who paid for the mint.

Impact: Enables NFT front-running and theft, causing financial and reputational damage


Description

  • In the normal flow, a user calls requestMintWeatherNFT() to initiate an NFT mint based on weather data. Once Chainlink Functions fulfill the weather request, the contract expects the user to call fulfillMintRequest() to complete the mint.


  • However, fulfillMintRequest() mints the NFT to msg.sender, not the original user who requested it. Since this function is public and not restricted to the original requester, anyone can call it once the request is fulfilled and steal the NFT, especially if they monitor the blockchain for incoming responses.

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());
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,
Weather(weather)
);
@> _mint(msg.sender, tokenId); // Vulnerability: NFT minted to caller, not original requester
s_tokenIdToWeather[tokenId] = Weather(weather);
uint256 upkeepId;
if (_userMintRequest.registerKeeper) {
// Register chainlink keeper to pull weather data in order to automate weather nft
LinkTokenInterface(s_link).approve(s_keeperRegistrar, _userMintRequest.initLinkDeposit);
IAutomationRegistrarInterface.RegistrationParams
memory _keeperParams = IAutomationRegistrarInterface
.RegistrationParams({
name: string.concat(
"Weather NFT Keeper: ",
Strings.toString(tokenId)
),
encryptedEmail: "",
upkeepContract: address(this),
gasLimit: s_upkeepGaslimit,
adminAddress: address(this),
triggerType: 0,
checkData: abi.encode(tokenId),
triggerConfig: "",
offchainConfig: "",
amount: uint96(_userMintRequest.initLinkDeposit)
});
upkeepId = IAutomationRegistrarInterface(s_keeperRegistrar)
.registerUpkeep(_keeperParams);
}
s_weatherNftInfo[tokenId] = WeatherNftInfo({
heartbeat: _userMintRequest.heartbeat,
lastFulfilledAt: block.timestamp,
upkeepId: upkeepId,
pincode: _userMintRequest.pincode,
isoCode: _userMintRequest.isoCode
});
}

Risk

Likelihood:

  • This will occur whenever an off-chain Chainlink Functions response is ready, and anyone (not just the original requester) calls fulfillMintRequest().

  • Front-running bots or malicious users can monitor the chain and frontrun the legitimate user to mint the NFT for themselves.

Impact:

  • The NFT is minted to the wrong address, causing a direct loss of funds/value to the original user.

  • Loss of user trust and protocol integrity due to unpredictable or malicious minting behavior.

Proof of Concept

Alice calls requestMintWeatherNFT(...) to mint a Weather NFT.

Her request is stored in the contract, and a Chainlink Functions job is triggered to fetch weather data.

Alice pays the mint price and, optionally, deposits LINK.

After some time, Chainlink Functions completes the job and returns a weather result.

The contract now expects someone to call fulfillMintRequest(requestId) to mint the NFT.

Mallory, a malicious user, watches the blockchain and sees that the weather data has been fulfilled.

She calls fulfillMintRequest(requestId) before Alice does.

Because the contract mints the NFT to msg.sender, not to the original requester (Alice), Mallory receives the NFT.

Recommended Mitigation

Track and enforce the original requester in fulfillMintRequest() by using s_funcReqIdToUserMintReq[requestId].user, rather than msg.sender, for the _mint() call:

address recipient = s_funcReqIdToUserMintReq[requestId].user;
require(recipient != address(0), "Invalid or expired request");
_mint(recipient, tokenId);
Updates

Appeal created

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