If an error in WeatherNft::fulfillMintRequest occurs the user can't withdraw the deposited Link.
Description:
If an error occurs in WeatherNft::fulfillMintRequest (e.g., due to invalid response data or other issues), the LINK tokens deposited by the user for Chainlink Keepers remain locked in the contract, with no way for the user to withdraw them.
Impact:
Users may lose their deposited LINK if the minting process fails, leading to financial loss and reduced trust in the platform.
Proof of Concept:
Add the following to the test suite:
unction testOnfulfillMintRequestFailDepositedLinkStucks() public {
string memory pincode = "0000";
string memory isoCode = "XX";
bool registerKeeper = true;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
uint256 tokenId = weatherNft.s_tokenCounter();
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
bytes32 reqId = weatherNft.requestMintWeatherNFT{value: weatherNft.s_currentMintPrice()}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
vm.prank(functionsRouter);
bytes memory weatherResponse = abi.encode(WeatherNftStore.Weather.RAINY);
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
vm.prank(user);
vm.expectRevert(WeatherNftStore.WeatherNft__Unauthorized.selector);
weatherNft.fulfillMintRequest(bytes32("6666"));
assertEq(linkToken.balanceOf(address(weatherNft)), 5e18);
}
Recommended Mitigation:
Add a withdraw functionality to WeatherNft that check the owner of the requestId.
Example:
On WeathersNftStore add:
// variables
uint256 public s_tokenCounter;
mapping(Weather => string) public s_weatherToTokenURI;
FunctionsConfig public s_functionsConfig;
mapping(bytes32 => UserMintRequest) public s_funcReqIdToUserMintReq;
mapping(bytes32 => MintFunctionReqResponse) public s_funcReqIdToMintFunctionReqResponse;
mapping(bytes32 => uint256) public s_funcReqIdToTokenIdUpdate;
+ mapping(address => uint256) addressToLinkDeposit;
On WeatherNft add:
// functions
+ function withdrawLinks(uint256 _tokenId) external {
+ address owner = _ownerOf(_tokenId);
+ if (owner != msg.sender) {
+ revert WeatherNft__Unauthorized();
+ } else {
+ LinkTokenInterface(s_link).approve(owner, addressToLinkDeposit[owner]);
+ IERC20(s_link).safeTransferFrom(address(this), address(this), addressToLinkDeposit[owner]);
+ }
+ }
function updateFunctionsGasLimit(uint32 newGaslimit) external onlyOwner {
s_functionsConfig.gasLimit = newGaslimit;
}
function updateSubId(uint64 newSubId) external onlyOwner {
s_functionsConfig.subId = newSubId;
}
function updateSource(string memory newSource) external onlyOwner {
s_functionsConfig.source = newSource;
}
function requestMintWeatherNFT( // check
string memory _pincode, string memory _isoCode, bool _registerKeeper, uint256 _heartbeat, uint256 _initLinkDeposit)
external
payable
returns (bytes32 _reqId)
{
require(msg.value == s_currentMintPrice, WeatherNft__InvalidAmountSent());
s_currentMintPrice += s_stepIncreasePerMint;
if (_registerKeeper) {
+ addressToLinkDeposit[msg.sender] = _initLinkDeposit;
IERC20(s_link).safeTransferFrom(msg.sender, address(this), _initLinkDeposit);
}
_reqId = _sendFunctionsWeatherFetchRequest(_pincode, _isoCode);
emit WeatherNFTMintRequestSent(msg.sender, _pincode, _isoCode, _reqId);
s_funcReqIdToUserMintReq[_reqId] = UserMintRequest({
user: msg.sender,
pincode: _pincode,
isoCode: _isoCode,
// e - using a keeper or not
registerKeeper: _registerKeeper,
heartbeat: _heartbeat,
initLinkDeposit: _initLinkDeposit
});
}