Skip to content

Commit 7937f9b

Browse files
authored
Merge pull request #383 from Caritajoe18/feature/commitment-marketplace-unit-tests-reentrancy-guard-on-marketplace-entry-points
feat(commitment_marketplace): unit-tests-reentrancy-guard-on-marketpl…
2 parents 792f465 + 8db8fbd commit 7937f9b

File tree

6 files changed

+465
-92
lines changed

6 files changed

+465
-92
lines changed

contracts/commitment_marketplace/src/lib.rs

Lines changed: 125 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
2+
//! # Commitment Marketplace Contract
3+
//!
4+
//! Soroban smart contract for NFT marketplace operations (listings, offers, auctions) with reentrancy guard and fee logic.
5+
//!
6+
//! ## Security
7+
//! - All state-changing entry points require authentication (`require_auth`).
8+
//! - Reentrancy guard is enforced on all external-call entry points.
9+
//! - Arithmetic is performed using checked math; see individual functions for overflow/underflow notes.
10+
//!
11+
//! ## Errors
12+
//! - See [`MarketplaceError`] for all error codes.
13+
//!
14+
//! ## Storage
15+
//!
16+
//! - See [`DataKey`] for all storage keys mutated by each entry point.
17+
//!
18+
//! ## Audit Notes
19+
//! - No cross-contract NFT ownership checks are performed in this implementation (see comments in code).
20+
//! - All token transfers use Soroban token interface.
21+
122
#![no_std]
223

324
use soroban_sdk::{
@@ -140,13 +161,14 @@ impl CommitmentMarketplace {
140161
// Initialization
141162
// ========================================================================
142163

143-
/// Initialize the marketplace
144-
///
145-
/// # Arguments
146-
/// * `admin` - Admin address
147-
/// * `nft_contract` - Address of the CommitmentNFT contract
148-
/// * `fee_basis_points` - Marketplace fee in basis points (e.g., 250 = 2.5%)
149-
/// * `fee_recipient` - Address to receive marketplace fees
164+
/// @notice Initialize the marketplace contract.
165+
/// @param admin Admin address (must sign the transaction).
166+
/// @param nft_contract Address of the CommitmentNFT contract.
167+
/// @param fee_basis_points Marketplace fee in basis points (e.g., 250 = 2.5%).
168+
/// @param fee_recipient Address to receive marketplace fees.
169+
/// @dev Only callable once. Sets up admin, NFT contract, fee, and fee recipient.
170+
/// @error MarketplaceError::AlreadyInitialized if already initialized.
171+
/// @security Only callable by `admin` (require_auth).
150172
pub fn initialize(
151173
e: Env,
152174
admin: Address,
@@ -184,15 +206,21 @@ impl CommitmentMarketplace {
184206
Ok(())
185207
}
186208

187-
/// Get admin address
209+
/// @notice Get the admin address for the marketplace.
210+
/// @return admin Address of the admin.
211+
/// @error MarketplaceError::NotInitialized if not initialized.
188212
pub fn get_admin(e: Env) -> Result<Address, MarketplaceError> {
189213
e.storage()
190214
.instance()
191215
.get(&DataKey::Admin)
192216
.ok_or(MarketplaceError::NotInitialized)
193217
}
194218

195-
/// Update marketplace fee (admin only)
219+
/// @notice Update the marketplace fee (basis points).
220+
/// @param fee_basis_points New fee in basis points.
221+
/// @dev Only callable by admin.
222+
/// @error MarketplaceError::NotInitialized if not initialized.
223+
/// @security Only callable by `admin` (require_auth).
196224
pub fn update_fee(e: Env, fee_basis_points: u32) -> Result<(), MarketplaceError> {
197225
let admin: Address = Self::get_admin(e.clone())?;
198226
admin.require_auth();
@@ -211,16 +239,16 @@ impl CommitmentMarketplace {
211239
// Listing Management
212240
// ========================================================================
213241

214-
/// List an NFT for sale
215-
///
216-
/// # Arguments
217-
/// * `seller` - The seller's address (must be NFT owner)
218-
/// * `token_id` - The NFT token ID to list
219-
/// * `price` - The sale price
220-
/// * `payment_token` - The token contract address for payment
221-
///
222-
/// # Reentrancy Protection
223-
/// Protected with reentrancy guard as it makes external NFT contract calls
242+
/// @notice List an NFT for sale on the marketplace.
243+
/// @param seller Seller's address (must be NFT owner and sign the transaction).
244+
/// @param token_id NFT token ID to list.
245+
/// @param price Sale price (must be > 0).
246+
/// @param payment_token Token contract address for payment.
247+
/// @dev Reentrancy guard enforced. No cross-contract NFT ownership check in this implementation.
248+
/// @error MarketplaceError::InvalidPrice if price <= 0.
249+
/// @error MarketplaceError::ListingExists if listing already exists.
250+
/// @error MarketplaceError::NotInitialized if contract not initialized.
251+
/// @security Only callable by `seller` (require_auth).
224252
pub fn list_nft(
225253
e: Env,
226254
seller: Address,
@@ -311,10 +339,13 @@ impl CommitmentMarketplace {
311339
Ok(())
312340
}
313341

314-
/// Cancel a listing
315-
///
316-
/// # Reentrancy Protection
317-
/// Uses checks-effects-interactions pattern
342+
/// @notice Cancel an active NFT listing.
343+
/// @param seller Seller's address (must sign the transaction).
344+
/// @param token_id NFT token ID to cancel listing for.
345+
/// @dev Reentrancy guard enforced. Checks-effects-interactions pattern.
346+
/// @error MarketplaceError::ListingNotFound if listing does not exist.
347+
/// @error MarketplaceError::NotSeller if caller is not the seller.
348+
/// @security Only callable by `seller` (require_auth).
318349
pub fn cancel_listing(e: Env, seller: Address, token_id: u32) -> Result<(), MarketplaceError> {
319350
// Reentrancy protection
320351
let guard: bool = e
@@ -377,14 +408,14 @@ impl CommitmentMarketplace {
377408
Ok(())
378409
}
379410

380-
/// Buy an NFT
381-
///
382-
/// # Arguments
383-
/// * `buyer` - The buyer's address
384-
/// * `token_id` - The NFT token ID to buy
385-
///
386-
/// # Reentrancy Protection
387-
/// Critical - handles token transfers. Protected with reentrancy guard.
411+
/// @notice Buy an NFT from an active listing.
412+
/// @param buyer Buyer's address (must sign the transaction).
413+
/// @param token_id NFT token ID to buy.
414+
/// @dev Reentrancy guard enforced. Handles token transfers. No cross-contract NFT transfer in this implementation.
415+
/// @error MarketplaceError::ListingNotFound if listing does not exist.
416+
/// @error MarketplaceError::CannotBuyOwnListing if buyer is seller.
417+
/// @error MarketplaceError::NotInitialized if contract not initialized.
418+
/// @security Only callable by `buyer` (require_auth).
388419
pub fn buy_nft(e: Env, buyer: Address, token_id: u32) -> Result<(), MarketplaceError> {
389420
// Reentrancy protection
390421
let guard: bool = e
@@ -497,15 +528,19 @@ impl CommitmentMarketplace {
497528
Ok(())
498529
}
499530

500-
/// Get a listing
531+
/// @notice Get details of a specific NFT listing.
532+
/// @param token_id NFT token ID.
533+
/// @return Listing struct.
534+
/// @error MarketplaceError::ListingNotFound if listing does not exist.
501535
pub fn get_listing(e: Env, token_id: u32) -> Result<Listing, MarketplaceError> {
502536
e.storage()
503537
.persistent()
504538
.get(&DataKey::Listing(token_id))
505539
.ok_or(MarketplaceError::ListingNotFound)
506540
}
507541

508-
/// Get all active listings
542+
/// @notice Get all active NFT listings.
543+
/// @return Vec<Listing> of all active listings.
509544
pub fn get_all_listings(e: Env) -> Vec<Listing> {
510545
let active_listings: Vec<u32> = e
511546
.storage()
@@ -532,10 +567,15 @@ impl CommitmentMarketplace {
532567
// Offer System
533568
// ========================================================================
534569

535-
/// Make an offer on an NFT
536-
///
537-
/// # Reentrancy Protection
538-
/// Protected with reentrancy guard
570+
/// @notice Make an offer on an NFT.
571+
/// @param offerer Offer maker's address (must sign the transaction).
572+
/// @param token_id NFT token ID to make offer on.
573+
/// @param amount Offer amount (must be > 0).
574+
/// @param payment_token Token contract address for payment.
575+
/// @dev Reentrancy guard enforced.
576+
/// @error MarketplaceError::InvalidOfferAmount if amount <= 0.
577+
/// @error MarketplaceError::OfferExists if offerer already has an offer.
578+
/// @security Only callable by `offerer` (require_auth).
539579
pub fn make_offer(
540580
e: Env,
541581
offerer: Address,
@@ -608,10 +648,14 @@ impl CommitmentMarketplace {
608648
Ok(())
609649
}
610650

611-
/// Accept an offer
612-
///
613-
/// # Reentrancy Protection
614-
/// Critical - handles token transfers. Protected with reentrancy guard.
651+
/// @notice Accept an offer on an NFT.
652+
/// @param seller Seller's address (must sign the transaction).
653+
/// @param token_id NFT token ID.
654+
/// @param offerer Address of the offer maker.
655+
/// @dev Reentrancy guard enforced. Handles token transfers. No cross-contract NFT transfer in this implementation.
656+
/// @error MarketplaceError::OfferNotFound if offer does not exist.
657+
/// @error MarketplaceError::NotInitialized if contract not initialized.
658+
/// @security Only callable by `seller` (require_auth).
615659
pub fn accept_offer(
616660
e: Env,
617661
seller: Address,
@@ -724,7 +768,11 @@ impl CommitmentMarketplace {
724768
Ok(())
725769
}
726770

727-
/// Cancel an offer
771+
/// @notice Cancel an offer made on an NFT.
772+
/// @param offerer Offer maker's address (must sign the transaction).
773+
/// @param token_id NFT token ID.
774+
/// @error MarketplaceError::OfferNotFound if offer does not exist.
775+
/// @security Only callable by `offerer` (require_auth).
728776
pub fn cancel_offer(e: Env, offerer: Address, token_id: u32) -> Result<(), MarketplaceError> {
729777
offerer.require_auth();
730778

@@ -755,7 +803,9 @@ impl CommitmentMarketplace {
755803
Ok(())
756804
}
757805

758-
/// Get all offers for a token
806+
/// @notice Get all offers for a specific NFT token.
807+
/// @param token_id NFT token ID.
808+
/// @return Vec<Offer> of all offers for the token.
759809
pub fn get_offers(e: Env, token_id: u32) -> Vec<Offer> {
760810
e.storage()
761811
.persistent()
@@ -767,10 +817,17 @@ impl CommitmentMarketplace {
767817
// Auction System
768818
// ========================================================================
769819

770-
/// Start an auction
771-
///
772-
/// # Reentrancy Protection
773-
/// Protected with reentrancy guard
820+
/// @notice Start an auction for an NFT.
821+
/// @param seller Seller's address (must sign the transaction).
822+
/// @param token_id NFT token ID.
823+
/// @param starting_price Starting price for the auction (must be > 0).
824+
/// @param duration_seconds Duration of the auction in seconds (must be > 0).
825+
/// @param payment_token Token contract address for payment.
826+
/// @dev Reentrancy guard enforced.
827+
/// @error MarketplaceError::InvalidPrice if starting price <= 0.
828+
/// @error MarketplaceError::InvalidDuration if duration is 0.
829+
/// @error MarketplaceError::ListingExists if auction already exists for token.
830+
/// @security Only callable by `seller` (require_auth).
774831
pub fn start_auction(
775832
e: Env,
776833
seller: Address,
@@ -858,10 +915,15 @@ impl CommitmentMarketplace {
858915
Ok(())
859916
}
860917

861-
/// Place a bid
862-
///
863-
/// # Reentrancy Protection
864-
/// Critical - handles token transfers for bid refunds. Protected with reentrancy guard.
918+
/// @notice Place a bid on an active auction.
919+
/// @param bidder Bidder's address (must sign the transaction).
920+
/// @param token_id NFT token ID.
921+
/// @param bid_amount Amount of the bid (must be > current bid).
922+
/// @dev Reentrancy guard enforced. Handles token transfers for bid refunds.
923+
/// @error MarketplaceError::AuctionEnded if auction has ended.
924+
/// @error MarketplaceError::BidTooLow if bid is not higher than current bid.
925+
/// @error MarketplaceError::CannotBuyOwnListing if seller tries to bid.
926+
/// @security Only callable by `bidder` (require_auth).
865927
pub fn place_bid(
866928
e: Env,
867929
bidder: Address,
@@ -953,10 +1015,12 @@ impl CommitmentMarketplace {
9531015
Ok(())
9541016
}
9551017

956-
/// End an auction
957-
///
958-
/// # Reentrancy Protection
959-
/// Critical - handles final settlement. Protected with reentrancy guard.
1018+
/// @notice End an auction and settle payment/NFT transfer.
1019+
/// @param token_id NFT token ID.
1020+
/// @dev Reentrancy guard enforced. Handles final settlement. Anyone can call after auction ends.
1021+
/// @error MarketplaceError::AuctionNotFound if auction does not exist.
1022+
/// @error MarketplaceError::AuctionNotEnded if auction has not ended yet.
1023+
/// @error MarketplaceError::AuctionEnded if auction already ended.
9601024
pub fn end_auction(e: Env, token_id: u32) -> Result<(), MarketplaceError> {
9611025
// Reentrancy protection
9621026
let guard: bool = e
@@ -1084,15 +1148,19 @@ impl CommitmentMarketplace {
10841148
Ok(())
10851149
}
10861150

1087-
/// Get auction details
1151+
/// @notice Get details of a specific auction.
1152+
/// @param token_id NFT token ID.
1153+
/// @return Auction struct.
1154+
/// @error MarketplaceError::AuctionNotFound if auction does not exist.
10881155
pub fn get_auction(e: Env, token_id: u32) -> Result<Auction, MarketplaceError> {
10891156
e.storage()
10901157
.persistent()
10911158
.get(&DataKey::Auction(token_id))
10921159
.ok_or(MarketplaceError::AuctionNotFound)
10931160
}
10941161

1095-
/// Get all active auctions
1162+
/// @notice Get all active auctions.
1163+
/// @return Vec<Auction> of all active auctions.
10961164
pub fn get_all_auctions(e: Env) -> Vec<Auction> {
10971165
let active_auctions: Vec<u32> = e
10981166
.storage()

0 commit comments

Comments
 (0)