Weather Witness

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

Critical Data Handling: Unhandled Failed Weather Data Responses

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; // Silent failure with no cleanup
}

Initial State:

  1. User has paid for NFT minting

  2. Request sent to Chainlink oracle

  3. Weather data response fails

Attack Scenario:

  1. User pays s_currentMintPrice to mint NFT

  2. Oracle returns empty/error response

  3. Function silently returns

  4. User funds remain locked

  5. Request state remains uncleaned

Proof of Concept

// Test file demonstrating the vulnerability
function testFailedWeatherData() public {
// Setup
uint256 initialBalance = address(user).balance;
uint256 mintPrice = weatherNft.s_currentMintPrice();
// User attempts to mint
vm.prank(user);
bytes32 reqId = weatherNft.requestMintWeatherNFT{value: mintPrice}(
"123456",
"US",
false,
3600,
0
);
// Simulate failed weather data
weatherNft.fulfillRequest(
reqId,
"", // empty response
"API Error" // error message
);
// Assert: User lost funds
assertEq(address(user).balance, initialBalance - mintPrice);
// Assert: Request still exists
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

  1. Implement refund mechanism:

contract WeatherNft {
mapping(bytes32 => bool) public failedRequests;
function fulfillMintRequest(bytes32 requestId) external {
// ...existing code...
if (response.length == 0 || err.length > 0) {
failedRequests[requestId] = true;
emit MintRequestFailed(requestId, err);
return;
}
// ...existing code...
}
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");
// Clean up state
delete failedRequests[requestId];
delete s_funcReqIdToUserMintReq[requestId];
delete s_funcReqIdToMintFunctionReqResponse[requestId];
// Refund payment
payable(msg.sender).transfer(s_currentMintPrice);
emit MintRefunded(requestId, msg.sender);
}
}
  1. 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;
}
// ...existing code...
}
}
Updates

Appeal created

bube Lead Judge 8 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Lost fee in case of Oracle failure

If Oracle fails, the `fulfillMintRequest` function will not return the payed fee for the token to the user.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.