Weather Witness

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

Front-Running Vulnerability in NFT Mint Price Mechanism

Root + Impact

The price for minting an NFT is checked and then immediately increased within the same transaction, creating a front-running vulnerability. Attackers can observe pending transactions and front-run them, causing legitimate users' transactions to fail due to insufficient payment.

Description

  • Users mint NFTs by sending the exact current price WeatherNftStore::s_currentMintPrice, which should increase after each successful mint.

  • The price is verified before being increased in the same transaction, allowing attackers to front-run user transactions by submitting their own with higher gas fees, causing the original transactions to fail when the price has increased.

function requestMintWeatherNFT(
// parameters
) external payable returns (bytes32 _reqId) {
@> require(msg.value == s_currentMintPrice, WeatherNft__InvalidAmountSent());
@> s_currentMintPrice += s_stepIncreasePerMint;
// Rest of function...
}

Risk

Likelihood:

  • MEV bots continuously monitor the mempool for such profitable front-running opportunities

  • During high demand periods, multiple users attempt to mint concurrently, creating more front-running targets

Impact:

  • Users pay gas for failed transactions without receiving an NFT

  • The minting process becomes frustrating and unreliable, damaging user experience and project reputation

Proof of Concept

This test demonstrates:

  1. Initial setup similar to existing tests

  2. The attacker observing the current mint price

  3. The attacker front-running by submitting a transaction at that price

  4. The price increasing after the attacker's transaction

  5. The user's transaction failing because the price has increased

  6. The user having to pay the higher price to successfully mint

function test_FrontRunningVulnerability() public {
// Create a new attacker address and fund it with LINK tokens
address attacker = makeAddr("attacker");
vm.deal(attacker, 1000e18);
deal(address(linkToken), attacker, 1000e18);
// Common parameters for mint
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = true;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
// Step 1: Record initial price
uint256 initialPrice = weatherNft.s_currentMintPrice();
// Step 2: User approves LINK for the mint
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.stopPrank();
// Step 3: Attacker front-runs with higher gas price
// In a real scenario, the attacker would see the user's transaction in the mempool
// and submit their own with higher gas price to get mined first
vm.startPrank(attacker);
linkToken.approve(address(weatherNft), initLinkDeposit);
weatherNft.requestMintWeatherNFT{value: initialPrice}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
// Step 4: Price has now increased
uint256 newPrice = weatherNft.s_currentMintPrice();
console.log("Price after attacker's transaction:", newPrice);
console.log("Price increase:", newPrice - initialPrice);
// Step 5: User's transaction with original price now fails
vm.startPrank(user);
vm.expectRevert(WeatherNftStore.WeatherNft__InvalidAmountSent.selector);
weatherNft.requestMintWeatherNFT{value: initialPrice}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
// Step 6: User has to retry with higher price to succeed
vm.startPrank(user);
weatherNft.requestMintWeatherNFT{value: newPrice}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
console.log("Front-running attack successful - user forced to pay higher price");
}

Recommended Mitigation

Price Lock Mechanism
A more robust solution is implementing a price lock mechanism that allows users to secure the current price for a limited time:

+ Struct PriceLock {
+ uint256 price;
+ uint256 expiryTime;
+ bool isActive;
+ }
+ mapping(addresss => PriceLock) public s_userPriceLock;
+ uint256 public priceLockDuration = 5 minutes;
+ // Add a function to lock in the current price
+ function lockMintPrice() external returns (uint256) {
+ s_userPriceLock[msg.sender] = PriceLock({
+ price: s_currentMintPrice;
+ expiryTime = block.timestamp + priceLockDuration;
+ isActive = true;
+ })
+ }
function requestMintWeatherNft(// parameters) external payable returns (bytes32 _reqId) {
- require(msg.valur == s_currentMintPrice, WeatherNft__InvalidAmountSent());
+ uint256 priceToCheck = s_currentMintPrice;
+ // check if user have an active price lock
+ PriceLock storage userLock = s_userPriceLock[msg.sender];
+ if(userLock.isActive == true && block.timestamp <= userLock.expiryTime) {
+ userLock.price = priceToCheck;
+ userLock.isActive = false;
+ }
+ require(msg.value == priceToCheck, WeatherNft__InvalidAmountSent());
+ // Rest of function
+ }
+ // Owner function to update the lock duration
+ function setPriceLockDuration(uint256 newDuration) external onlyOwner {
+ s_priceLockDuration = newDuration;
+ }
Updates

Appeal created

bube Lead Judge 5 days ago
Submission Judgement Published
Validated
Assigned finding tags:

The price of the token is increased before the token is minted

Support

FAQs

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