Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

`BidBeastsNFTMarket::listNFT` Does Not Follow CEI Pattern

Root + Impact

  • Root Cause: The listNFT function in the BidBeastsNFTMarket contract violates the Checks-Effects-Interactions (CEI) pattern by performing an external call to BBERC721.transferFrom before updating internal state. The CEI pattern requires that all state changes (effects) occur after checks and before external interactions to prevent reentrancy attacks. The current implementation makes the external call first, leaving the contract vulnerable to reentrancy if the called contract (e.g., a malicious ERC-721 token) reenters the function.

  • Impact: This violation exposes the contract to potential reentrancy attacks, where a malicious ERC-721 contract could reenter listNFT or other functions, leading to double listings, inconsistent state, or NFT loss. While the impact is limited by the low severity and specific conditions required for exploitation, it could disrupt marketplace operations and erode user trust if exploited.

Description:

This violates the Checks-Effects-Interactions (CEI) pattern, which dictates that all state changes should occur before making external calls. Failing to follow CEI can allow malicious contracts to exploit reentrancy during the external call.

  1. Checks – validate inputs and conditions.

  2. Effects – update internal state.

  3. Interactions – make external calls only after state updates.

The BidBeastsNFTMarket::listNFT function transfers an NFT to the contract using:

function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
// written-gas why use require instead of custom error?
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
if (_buyNowPrice > 0) {
require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
}
@> BBERC721.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
buyNowPrice: _buyNowPrice,
auctionEnd: 0, // Timer starts only after the first valid bid.
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}

Risk:

  • Likelihood: Low. The vulnerability requires a malicious ERC-721 token contract to be used, which depends on the seller interacting with a compromised or attacker-controlled NFT. This scenario is uncommon but possible in an open marketplace where users may list NFTs from untrusted sources.

  • Impact: Low. The impact is limited to specific reentrancy scenarios, with potential for state inconsistencies or NFT loss, but it does not inherently cause direct financial loss unless exploited in combination with other vulnerabilities. The primary risk is operational disruption and reduced trust.

Proof of Concept:

The vulnerability arises from the CEI pattern violation, where the external call to BBERC721.transferFrom precedes state updates, creating a reentrancy risk. This can be explained as follows:

  • Reentrancy Opportunity: When BBERC721.transferFrom is called, a malicious ERC-721 contract can implement onERC721Received to reenter listNFT before the listings[tokenId] update. During this reentry, the attacker could call listNFT again with the same tokenId, potentially overwriting the listings mapping or triggering unintended behavior, such as listing the NFT multiple times under different parameters.

  • State Inconsistency: If reentrancy occurs, the listings[tokenId] might not reflect the intended seller, minPrice, or buyNowPrice from the original call, leading to an inconsistent state. For example, an attacker could set a lower minPrice or a different seller, disrupting the auction process.

  • NFT Loss Risk: In extreme cases, a malicious contract could manipulate the transfer logic or reenter to transfer the NFT elsewhere before the state is updated, though this depends on the specific ERC-721 implementation and additional vulnerabilities.

  • Importance of CEI: The CEI pattern prevents such issues by ensuring state changes are finalized before external calls, making reentrancy ineffective. The current order allows the external call to act as a gateway for reentry, highlighting the need to reorder the logic to align with best practices.

This proof demonstrates that the lack of CEI adherence creates a subtle but exploitable weakness, particularly in an open marketplace where NFT sources may vary.

Recommended Mitigation:

  • Refactor listNFT to update internal state before performing external calls:

function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
+ // --- CHECKS ---
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
if (_buyNowPrice > 0) {
require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
}
- BBERC721.transferFrom(msg.sender, address(this), tokenId);
+ // --- EFFECTS ---
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
buyNowPrice: _buyNowPrice,
auctionEnd: 0,
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
+ // --- INTERACTIONS ---
+ BBERC721.transferFrom(msg.sender, address(this), tokenId);
}

References:

  1. Solidity Security Patterns – Checks-Effects-Interactions (CEI)

  2. Consensys Best Practices – Reentrancy

  3. OWASP Smart Contract Security Cheat Sheet

Updates

Lead Judging Commences

cryptoghost Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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