Cyfrin First Fight 40 - Weather Witness
This project provides a dynamic NFT system that changes based on real-world weather conditions for specified locations using Chainlink Functions, Automation, and ERC721 standards. Users can mint their weather NFT and automate the weather updates using Chainlink Automation.
This article describes the First Fight 40 from Cyfrin.
The code is available on GitHub.
[TOC]
Description
-
Weather NFT enables users to mint NFTs tied to specific geographic locations (identified by pincode and ISO country code). These NFTs automatically update to reflect the current weather conditions of their linked location through Chainlink’s decentralized oracle network. The project leverages Chainlink Functions to fetch weather data and Chainlink Automation to keep the NFTs up-to-date.
The codebase is primarily built around two main contracts:
WeatherNft.sol- Main contract for minting and managing dynamic weather NFTsWeatherNftStore.sol- Base contract containing storage variables, events, and data structures
WeatherNft
This contract allows users to:
- Mint NFTs linked to specific geographic locations User can mint weather NFT by paying the mint price. The smart contract also allows user to automate the weather updations in order to update the NFT at regular intervals on the basis of heartbeat. The user can specify whether they want to register automation via chainlink automation by passing _registerKeeper as true. They are required to make an initial link deposit which will fund their keeper subscription. A dedicated keeper subcription is made for every NFT having tokenId as the checkData.
- Weather Query Chainlink functions is utilized to send a request to Open Weather API and fetch the current weather. The WeatherNft takes in user’s pincode and isocode to fetch the current weather. The implementation to get the current weather on the basis of pincode and isocode is written in GetWeather.js Related Links for OpenWeather API:
https://openweathermap.org/api/geocoding-api https://openweathermap.org/current https://openweathermap.org/weather-conditions- Set up automated weather updates using Chainlink Automation The checkupkeep and performupkeep function performs the automation task for weather NFT auto updation with the current temperature. The checkupkeep function is triggered by chainlink keepers with the respective tokenId to check for NFT update if interval has passed depicted by heartbeat. The keepers then call performupkeep function to send a request to chainlink functions to fetch the current weather and update the NFT data.
- Manual Weather Update A user can also choose to not subscribe to automation and can manually call performupkeep with the tokenId of their NFT and get their NFT updated with the latest data from the chainlink functions request to open weather API.
WeatherNftStore
This contract contains:
- Storage variables for the WeatherNft system
- Event definitions
- Error declarations
- Data structure definitions
Minting a Weather NFT
A user calls the
requestMintWeatherNFTfunction with:- Pincode - postal/zip code of the location
- ISO country code - country identifier
- Optional Keeper registration - to automate weather updates
- Heartbeat interval - frequency of weather updates
- Initial LINK deposit - for automation services
The minting process involves:
- Payment of the current mint price (which increases with each mint)
- Sending a Chainlink Functions request to fetch weather data
- Chainlink function callback populates the state with the response and error containing the weather data.
- User calls the fulfillMintRequest function and NFT is minted with the current weather data in response.
- If requested, a Chainlink Automation upkeep is registered to keep the NFT updated
Automating Weather Updates
For NFTs with registered automation:
- Chainlink Keepers check if the heartbeat interval has passed
- When it’s time for an update, the keeper calls
performUpkeep - A new Chainlink Functions request is sent to fetch the current weather
- When the data arrives, the NFT’s weather state is updated
Weather States
The contract supports the following weather states:
- SUNNY
- CLOUDY
- RAINY
- THUNDERSTORM
- WINDY
- SNOW
Each weather state has an associated image URI that represents the current condition. The sample URI can be found in the deployment script (deploy/DeployWeatherNft.js)
Roles in the Project:
- NFT Owner
- Users who mint and own Weather NFTs
- Can transfer ownership of their NFTs
- Contract Owner
- Can update Chainlink Functions configuration
- Can modify gas limits and other system parameters
- Controls subscription IDs and other administrative settings
All submissions
My submissions
Heigh - PerformUpkeep should only be performed when checkUpkeep return true
Root + Impact
PerformUpkeep does not call checkUpkeep
As a result, it is possible to update the NFT weather state before the heartbeat.
Description
Keeper will call checkUpKeep before perming the call to PerformUpkeep. But since there is no access control on the function, anybody can call the function even if checkUpKeep return false.
// Root cause in the codebase with @> marks to highlight the relevant section
Risk
Likelihood:
Interest in an attacker is probably low
Impact:
NFT weather state is updated before the heartbeat
Recommended Mitigation
Call checkUpKeep inside performUpkeep
\- remove this code
\+ add this code
function performUpkeep(bytes calldata performData) external override {
bool res = checkUpkeep(performData)
require(res, "Invalid update")
}
Heigh - NFT price (ether) is locked in the smart contract (Lack of Withdraw function)
Root + Impact
The contract includes a payable function to accept Ether but lacks a corresponding function to withdraw it, which leads to the Ether being locked in the contract.
Description
requestMintWeatherNFT is payable where msg.value == s_currentMintPrice
The value represents the NFT price and it is locked in the smart contract
function requestMintWeatherNFT(
string memory _pincode,
string memory _isoCode,
bool _registerKeeper,
uint256 _heartbeat,
uint256 _initLinkDeposit
) external payable returns (bytes32 _reqId) {
require(
msg.value == s_currentMintPrice,
WeatherNft__InvalidAmountSent()
);
Risk
Likelihood:
For each NFT minted, the NFT price will be locked in the smart contract
Impact:
Ethers payed for each mint is locked inside the smart contract
Recommended Mitigation
Implement a public or external function that allows for the withdrawal of Ether from the contract.
- remove this code
+ add this code
function withdraw(uint256 amount) external onlyOwner{
require(amount > 0, "Nothing to withdraw");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Withdraw failed");
}
Invalid submissions
Low - Conversion - loss of precision for link deposit
Reason: incorrect statement
- After discussion with the sponsor, this turns out to be invalid. This is because the Chainlink has a capped maximum supply of 1 billion LINK tokens. This means that the total number of LINK tokens will never exceed 1 billion. The token has 18 decimals, so the max scaled value that is required to represent all LINK tokens is 1e27. The max value of
uint96is 2**96 - 1, that is around 79e27 and it is sufficient to store all LINK tokens. Therefore, the cast from uint256 to uint96 is safe and there is no possibility of token truncation/loss of tokens.
Root + Impact
`initLinkDeposit, a uint256,is converted to an uint96 in FullFillMintRequest.
There is a potentiel of incorrect recording of deposit funds if the user put an amount > 2^96 as a deposit when he creates is NFT
Description
When creating the parameters for the keeper, the value of initLinkDeposit which represents the initial link deposit by the user during the mint is converted to a uint96 while the current unit is uint256.
IAutomationRegistrarInterface.RegistrationParams
memory _keeperParams = IAutomationRegistrarInterface
.RegistrationParams({
name: string.concat(
"Weather NFT Keeper: ",
Strings.toString(tokenId)
),
encryptedEmail: "",
upkeepContract: address(this),
gasLimit: s_upkeepGaslimit,
adminAddress: address(this),
triggerType: 0,
checkData: abi.encode(tokenId),
triggerConfig: "",
offchainConfig: "",
// @audit : uint256 - uint96 conversion
amount: uint96(_userMintRequest.initLinkDeposit)
});
Risk
Likelihood:
the user makes an initial deposit > 2^96 (low)
Impact:
The check will depend of how this amount is used by the keeper, but it is probably to know the amount deposited by the user.
Proof of Concept
Use a deposit > 2^96
Recommended Mitigation
Use an uint96 for the function parameter in requestMintWeather NFT and in the corresponding struct UserMintRequest
- remove this code
function requestMintWeatherNFT(
[...]
uint256 _initLinkDeposit
) external payable returns (bytes32 _reqId)
struct UserMintRequest {
[...]
uint256 initLinkDeposit;
string pincode;
string isoCode;
}
+ add this code
function requestMintWeatherNFT(
[...]
uint96 _initLinkDeposit
) external payable returns (bytes32 _reqId)
struct UserMintRequest {
[...]
uint96 initLinkDeposit;
string pincode;
string isoCode;
}