Description
The fulfillMintRequest
function fails to properly handle cases where weather data requests fail, leading to stuck transactions and lost funds.
Risk
Severity: High
Likelihood: Medium
Summary
When weather data requests fail, the function simply returns without proper error handling, refunds, or state cleanup.
Vulnerability Details
Root Cause:
if (response.length == 0 || err.length > 0) {
return;
}
Initial State:
User has paid for NFT minting
Request sent to Chainlink oracle
Weather data response fails
Attack Scenario:
User pays s_currentMintPrice
to mint NFT
Oracle returns empty/error response
Function silently returns
User funds remain locked
Request state remains uncleaned
Proof of Concept
function testFailedWeatherData() public {
uint256 initialBalance = address(user).balance;
uint256 mintPrice = weatherNft.s_currentMintPrice();
vm.prank(user);
bytes32 reqId = weatherNft.requestMintWeatherNFT{value: mintPrice}(
"123456",
"US",
false,
3600,
0
);
weatherNft.fulfillRequest(
reqId,
"",
"API Error"
);
assertEq(address(user).balance, initialBalance - mintPrice);
assertTrue(weatherNft.s_funcReqIdToUserMintReq(reqId).user != address(0));
}
Impact
-
Users lose funds permanently
-
Requests remain in pending state
-
No notification of failure
-
Contract state becomes cluttered
-
Poor user experience
Tools Used
-
Manual Review
-
Unit Testing
Recommendations
Implement refund mechanism:
contract WeatherNft {
mapping(bytes32 => bool) public failedRequests;
function fulfillMintRequest(bytes32 requestId) external {
if (response.length == 0 || err.length > 0) {
failedRequests[requestId] = true;
emit MintRequestFailed(requestId, err);
return;
}
}
function refundFailedMint(bytes32 requestId) external {
require(failedRequests[requestId], "Request not failed");
UserMintRequest memory request = s_funcReqIdToUserMintReq[requestId];
require(request.user == msg.sender, "Not request owner");
delete failedRequests[requestId];
delete s_funcReqIdToUserMintReq[requestId];
delete s_funcReqIdToMintFunctionReqResponse[requestId];
payable(msg.sender).transfer(s_currentMintPrice);
emit MintRefunded(requestId, msg.sender);
}
}
Alternative Implementation with Automatic Retries:
contract WeatherNft {
uint8 public constant MAX_RETRIES = 3;
mapping(bytes32 => uint8) private retryCount;
function fulfillMintRequest(bytes32 requestId) external {
if (response.length == 0 || err.length > 0) {
if (retryCount[requestId] < MAX_RETRIES) {
retryCount[requestId]++;
_sendFunctionsWeatherFetchRequest(
s_funcReqIdToUserMintReq[requestId].pincode,
s_funcReqIdToUserMintReq[requestId].isoCode
);
emit RequestRetried(requestId, retryCount[requestId]);
} else {
failedRequests[requestId] = true;
emit MintRequestFailedPermanently(requestId);
}
return;
}
}
}