Weather Witness

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

Minting Process Front-Running

Minting Process Involves Two Separate Transactions Leads To Potential Front-Running

Description

  • The minting process for an NFT is split into two transactions: first, requestMintWeatherNFT initiates a Chainlink Functions request and takes payment, and second, fulfillMintRequest is intended to be called by the user after the Chainlink request is fulfilled to mint the NFT. However, the fulfillMintRequest function lacks proper access control. Anyone can call fulfillMintRequest with a valid _reqId once the Chainlink Functions response data is available on-chain, allowing an attacker to front-run the legitimate user and mint the NFT to their own address instead of the original requester's.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood: High

  • The Chainlink Functions fulfillment process makes the response data available on-chain via the handleOracleFulfillment (and subsequently fulfillRequest) callback, which is visible to anyone monitoring the blockchain.

  • An attacker can easily obtain the requestId from the WeatherNFTMintRequestSent event emitted by the requestMintWeatherNFT function call and monitor for the Chainlink fulfillment. Once fulfilled, they can call the unrestricted fulfillMintRequest function with the known requestId.

Impact: High

  • An attacker can steal the NFT that the legitimate user paid for and initiated the request for by calling fulfillMintRequest before the legitimate user.

  • The legitimate user suffers financial loss, having paid the mint price (and potentially a LINK deposit for keeper upkeep) but receiving no NFT. This also erodes user trust in the protocol.

Proof of Concept

The provided test case accurately demonstrates the vulnerability:

function test_bobFrontrunning() public {
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = true;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
uint256 tokenId = weatherNft.s_tokenCounter();
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{value: weatherNft.s_currentMintPrice()}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
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));
vm.prank(functionsRouter);
bytes memory weatherResponse = abi.encode(WeatherNftStore.Weather.RAINY);
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
vm.prank(bob); // bob is frontrunning the request
weatherNft.fulfillMintRequest(reqId);
assertEq(weatherNft.ownerOf(tokenId), bob);
}

Recommended Mitigation

Add a check at the beginning of the fulfillMintRequest function to ensure that msg.sender is the same address as the user who initiated the request, retrieved from the s_funcReqIdToUserMintReq mapping.

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];
+ require(_userMintRequest.user == msg.sender, WeatherNft__Unauthorized());
// ... rest of logic
}
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.