Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Invalid

NFT Auction DoS Through Malicious Highest Bidder

Relevant Context

The NFTLiquidator contract implements an auction mechanism for liquidated NFTs. Users can place bids using the placeBid() function, which automatically refunds the previous highest bidder when a new higher bid is placed.

Finding Description

The placeBid() function uses Solidity's native transfer() to refund the previous highest bidder. This creates a potential denial-of-service vulnerability because transfer() only provides 2300 gas stipend to the recipient, which is insufficient if the recipient is a contract with a complex receive() or fallback() function.

If the current highest bidder is a malicious contract that intentionally fails to accept ETH transfers (by implementing a receive() function that reverts or consumes more than 2300 gas), subsequent bidders will be unable to place new bids as the refund transaction will always fail.

Impact Explanation

High. This vulnerability can completely freeze the auction mechanism for any NFT where a malicious contract becomes the highest bidder. The auction cannot proceed as new bids will always fail, and the NFT becomes permanently stuck in the auction state unless bought back through the buyBackNFT() function at a premium.

Likelihood Explanation

High. The attack requires minimal resources (just deploying a simple contract) and has no external dependencies or timing constraints. Any actor can execute it at any time during an auction.

Proof of Concept

  1. Attacker deploys a contract with a receive() function that always reverts:

contract MaliciousBidder {
receive() external payable {
revert("Sorry :)");
}
function placeBid(address liquidator, uint256 tokenId) external payable {
NFTLiquidator(liquidator).placeBid{value: msg.value}(tokenId);
}
}
  1. Attacker uses the malicious contract to place a bid on a valuable NFT

  2. Any subsequent attempt to place a higher bid will fail because the contract cannot refund the attacker's bid

  3. The NFT remains stuck in auction until someone pays the buyback premium

Recommendation

Replace the transfer() call with a "pull over push" pattern where bid refunds are stored in a mapping and claimed separately:

contract NFTLiquidator {
// Add mapping for pending refunds
mapping(address => uint256) public pendingRefunds;
function placeBid(uint256 tokenId) external payable {
TokenData storage data = tokenData[tokenId];
if (block.timestamp >= data.auctionEndTime) revert AuctionHasEnded();
uint256 minBidAmount = data.highestBid + (data.highestBid * minBidIncreasePercentage / 100);
if (msg.value <= minBidAmount) revert BidTooLow(minBidAmount);
if (data.highestBidder != address(0)) {
// Store refund instead of immediate transfer
pendingRefunds[data.highestBidder] += data.highestBid;
}
data.highestBid = msg.value;
data.highestBidder = msg.sender;
emit BidPlaced(tokenId, msg.sender, msg.value);
}
// Add function to claim refunds
function claimRefund() external {
uint256 refundAmount = pendingRefunds[msg.sender];
if (refundAmount > 0) {
pendingRefunds[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: refundAmount}("");
if (!success) revert RefundFailed();
}
}
}

This pattern allows the auction to proceed normally even if a bidder cannot receive refunds, as they can claim their refund later using the claimRefund() function.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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