diff --git a/contracts/commitment_marketplace/src/lib.rs b/contracts/commitment_marketplace/src/lib.rs
index dc9a4590..4af446c1 100644
--- a/contracts/commitment_marketplace/src/lib.rs
+++ b/contracts/commitment_marketplace/src/lib.rs
@@ -1,3 +1,24 @@
+
+//! # Commitment Marketplace Contract
+//!
+//! Soroban smart contract for NFT marketplace operations (listings, offers, auctions) with reentrancy guard and fee logic.
+//!
+//! ## Security
+//! - All state-changing entry points require authentication (`require_auth`).
+//! - Reentrancy guard is enforced on all external-call entry points.
+//! - Arithmetic is performed using checked math; see individual functions for overflow/underflow notes.
+//!
+//! ## Errors
+//! - See [`MarketplaceError`] for all error codes.
+//!
+//! ## Storage
+//!
+//! - See [`DataKey`] for all storage keys mutated by each entry point.
+//!
+//! ## Audit Notes
+//! - No cross-contract NFT ownership checks are performed in this implementation (see comments in code).
+//! - All token transfers use Soroban token interface.
+
#![no_std]
use soroban_sdk::{
@@ -140,13 +161,14 @@ impl CommitmentMarketplace {
// Initialization
// ========================================================================
- /// Initialize the marketplace
- ///
- /// # Arguments
- /// * `admin` - Admin address
- /// * `nft_contract` - Address of the CommitmentNFT contract
- /// * `fee_basis_points` - Marketplace fee in basis points (e.g., 250 = 2.5%)
- /// * `fee_recipient` - Address to receive marketplace fees
+ /// @notice Initialize the marketplace contract.
+ /// @param admin Admin address (must sign the transaction).
+ /// @param nft_contract Address of the CommitmentNFT contract.
+ /// @param fee_basis_points Marketplace fee in basis points (e.g., 250 = 2.5%).
+ /// @param fee_recipient Address to receive marketplace fees.
+ /// @dev Only callable once. Sets up admin, NFT contract, fee, and fee recipient.
+ /// @error MarketplaceError::AlreadyInitialized if already initialized.
+ /// @security Only callable by `admin` (require_auth).
pub fn initialize(
e: Env,
admin: Address,
@@ -184,7 +206,9 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Get admin address
+ /// @notice Get the admin address for the marketplace.
+ /// @return admin Address of the admin.
+ /// @error MarketplaceError::NotInitialized if not initialized.
pub fn get_admin(e: Env) -> Result
{
e.storage()
.instance()
@@ -192,7 +216,11 @@ impl CommitmentMarketplace {
.ok_or(MarketplaceError::NotInitialized)
}
- /// Update marketplace fee (admin only)
+ /// @notice Update the marketplace fee (basis points).
+ /// @param fee_basis_points New fee in basis points.
+ /// @dev Only callable by admin.
+ /// @error MarketplaceError::NotInitialized if not initialized.
+ /// @security Only callable by `admin` (require_auth).
pub fn update_fee(e: Env, fee_basis_points: u32) -> Result<(), MarketplaceError> {
let admin: Address = Self::get_admin(e.clone())?;
admin.require_auth();
@@ -211,16 +239,16 @@ impl CommitmentMarketplace {
// Listing Management
// ========================================================================
- /// List an NFT for sale
- ///
- /// # Arguments
- /// * `seller` - The seller's address (must be NFT owner)
- /// * `token_id` - The NFT token ID to list
- /// * `price` - The sale price
- /// * `payment_token` - The token contract address for payment
- ///
- /// # Reentrancy Protection
- /// Protected with reentrancy guard as it makes external NFT contract calls
+ /// @notice List an NFT for sale on the marketplace.
+ /// @param seller Seller's address (must be NFT owner and sign the transaction).
+ /// @param token_id NFT token ID to list.
+ /// @param price Sale price (must be > 0).
+ /// @param payment_token Token contract address for payment.
+ /// @dev Reentrancy guard enforced. No cross-contract NFT ownership check in this implementation.
+ /// @error MarketplaceError::InvalidPrice if price <= 0.
+ /// @error MarketplaceError::ListingExists if listing already exists.
+ /// @error MarketplaceError::NotInitialized if contract not initialized.
+ /// @security Only callable by `seller` (require_auth).
pub fn list_nft(
e: Env,
seller: Address,
@@ -311,10 +339,13 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Cancel a listing
- ///
- /// # Reentrancy Protection
- /// Uses checks-effects-interactions pattern
+ /// @notice Cancel an active NFT listing.
+ /// @param seller Seller's address (must sign the transaction).
+ /// @param token_id NFT token ID to cancel listing for.
+ /// @dev Reentrancy guard enforced. Checks-effects-interactions pattern.
+ /// @error MarketplaceError::ListingNotFound if listing does not exist.
+ /// @error MarketplaceError::NotSeller if caller is not the seller.
+ /// @security Only callable by `seller` (require_auth).
pub fn cancel_listing(e: Env, seller: Address, token_id: u32) -> Result<(), MarketplaceError> {
// Reentrancy protection
let guard: bool = e
@@ -377,14 +408,14 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Buy an NFT
- ///
- /// # Arguments
- /// * `buyer` - The buyer's address
- /// * `token_id` - The NFT token ID to buy
- ///
- /// # Reentrancy Protection
- /// Critical - handles token transfers. Protected with reentrancy guard.
+ /// @notice Buy an NFT from an active listing.
+ /// @param buyer Buyer's address (must sign the transaction).
+ /// @param token_id NFT token ID to buy.
+ /// @dev Reentrancy guard enforced. Handles token transfers. No cross-contract NFT transfer in this implementation.
+ /// @error MarketplaceError::ListingNotFound if listing does not exist.
+ /// @error MarketplaceError::CannotBuyOwnListing if buyer is seller.
+ /// @error MarketplaceError::NotInitialized if contract not initialized.
+ /// @security Only callable by `buyer` (require_auth).
pub fn buy_nft(e: Env, buyer: Address, token_id: u32) -> Result<(), MarketplaceError> {
// Reentrancy protection
let guard: bool = e
@@ -497,7 +528,10 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Get a listing
+ /// @notice Get details of a specific NFT listing.
+ /// @param token_id NFT token ID.
+ /// @return Listing struct.
+ /// @error MarketplaceError::ListingNotFound if listing does not exist.
pub fn get_listing(e: Env, token_id: u32) -> Result {
e.storage()
.persistent()
@@ -505,7 +539,8 @@ impl CommitmentMarketplace {
.ok_or(MarketplaceError::ListingNotFound)
}
- /// Get all active listings
+ /// @notice Get all active NFT listings.
+ /// @return Vec of all active listings.
pub fn get_all_listings(e: Env) -> Vec {
let active_listings: Vec = e
.storage()
@@ -532,10 +567,15 @@ impl CommitmentMarketplace {
// Offer System
// ========================================================================
- /// Make an offer on an NFT
- ///
- /// # Reentrancy Protection
- /// Protected with reentrancy guard
+ /// @notice Make an offer on an NFT.
+ /// @param offerer Offer maker's address (must sign the transaction).
+ /// @param token_id NFT token ID to make offer on.
+ /// @param amount Offer amount (must be > 0).
+ /// @param payment_token Token contract address for payment.
+ /// @dev Reentrancy guard enforced.
+ /// @error MarketplaceError::InvalidOfferAmount if amount <= 0.
+ /// @error MarketplaceError::OfferExists if offerer already has an offer.
+ /// @security Only callable by `offerer` (require_auth).
pub fn make_offer(
e: Env,
offerer: Address,
@@ -608,10 +648,14 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Accept an offer
- ///
- /// # Reentrancy Protection
- /// Critical - handles token transfers. Protected with reentrancy guard.
+ /// @notice Accept an offer on an NFT.
+ /// @param seller Seller's address (must sign the transaction).
+ /// @param token_id NFT token ID.
+ /// @param offerer Address of the offer maker.
+ /// @dev Reentrancy guard enforced. Handles token transfers. No cross-contract NFT transfer in this implementation.
+ /// @error MarketplaceError::OfferNotFound if offer does not exist.
+ /// @error MarketplaceError::NotInitialized if contract not initialized.
+ /// @security Only callable by `seller` (require_auth).
pub fn accept_offer(
e: Env,
seller: Address,
@@ -724,7 +768,11 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Cancel an offer
+ /// @notice Cancel an offer made on an NFT.
+ /// @param offerer Offer maker's address (must sign the transaction).
+ /// @param token_id NFT token ID.
+ /// @error MarketplaceError::OfferNotFound if offer does not exist.
+ /// @security Only callable by `offerer` (require_auth).
pub fn cancel_offer(e: Env, offerer: Address, token_id: u32) -> Result<(), MarketplaceError> {
offerer.require_auth();
@@ -755,7 +803,9 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Get all offers for a token
+ /// @notice Get all offers for a specific NFT token.
+ /// @param token_id NFT token ID.
+ /// @return Vec of all offers for the token.
pub fn get_offers(e: Env, token_id: u32) -> Vec {
e.storage()
.persistent()
@@ -767,10 +817,17 @@ impl CommitmentMarketplace {
// Auction System
// ========================================================================
- /// Start an auction
- ///
- /// # Reentrancy Protection
- /// Protected with reentrancy guard
+ /// @notice Start an auction for an NFT.
+ /// @param seller Seller's address (must sign the transaction).
+ /// @param token_id NFT token ID.
+ /// @param starting_price Starting price for the auction (must be > 0).
+ /// @param duration_seconds Duration of the auction in seconds (must be > 0).
+ /// @param payment_token Token contract address for payment.
+ /// @dev Reentrancy guard enforced.
+ /// @error MarketplaceError::InvalidPrice if starting price <= 0.
+ /// @error MarketplaceError::InvalidDuration if duration is 0.
+ /// @error MarketplaceError::ListingExists if auction already exists for token.
+ /// @security Only callable by `seller` (require_auth).
pub fn start_auction(
e: Env,
seller: Address,
@@ -858,10 +915,15 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Place a bid
- ///
- /// # Reentrancy Protection
- /// Critical - handles token transfers for bid refunds. Protected with reentrancy guard.
+ /// @notice Place a bid on an active auction.
+ /// @param bidder Bidder's address (must sign the transaction).
+ /// @param token_id NFT token ID.
+ /// @param bid_amount Amount of the bid (must be > current bid).
+ /// @dev Reentrancy guard enforced. Handles token transfers for bid refunds.
+ /// @error MarketplaceError::AuctionEnded if auction has ended.
+ /// @error MarketplaceError::BidTooLow if bid is not higher than current bid.
+ /// @error MarketplaceError::CannotBuyOwnListing if seller tries to bid.
+ /// @security Only callable by `bidder` (require_auth).
pub fn place_bid(
e: Env,
bidder: Address,
@@ -953,10 +1015,12 @@ impl CommitmentMarketplace {
Ok(())
}
- /// End an auction
- ///
- /// # Reentrancy Protection
- /// Critical - handles final settlement. Protected with reentrancy guard.
+ /// @notice End an auction and settle payment/NFT transfer.
+ /// @param token_id NFT token ID.
+ /// @dev Reentrancy guard enforced. Handles final settlement. Anyone can call after auction ends.
+ /// @error MarketplaceError::AuctionNotFound if auction does not exist.
+ /// @error MarketplaceError::AuctionNotEnded if auction has not ended yet.
+ /// @error MarketplaceError::AuctionEnded if auction already ended.
pub fn end_auction(e: Env, token_id: u32) -> Result<(), MarketplaceError> {
// Reentrancy protection
let guard: bool = e
@@ -1084,7 +1148,10 @@ impl CommitmentMarketplace {
Ok(())
}
- /// Get auction details
+ /// @notice Get details of a specific auction.
+ /// @param token_id NFT token ID.
+ /// @return Auction struct.
+ /// @error MarketplaceError::AuctionNotFound if auction does not exist.
pub fn get_auction(e: Env, token_id: u32) -> Result {
e.storage()
.persistent()
@@ -1092,7 +1159,8 @@ impl CommitmentMarketplace {
.ok_or(MarketplaceError::AuctionNotFound)
}
- /// Get all active auctions
+ /// @notice Get all active auctions.
+ /// @return Vec of all active auctions.
pub fn get_all_auctions(e: Env) -> Vec {
let active_auctions: Vec = e
.storage()
diff --git a/contracts/commitment_marketplace/src/tests.rs b/contracts/commitment_marketplace/src/tests.rs
index 6127f87d..6ef9f474 100644
--- a/contracts/commitment_marketplace/src/tests.rs
+++ b/contracts/commitment_marketplace/src/tests.rs
@@ -1,3 +1,19 @@
+
+//! # Commitment Marketplace Contract Tests
+//!
+//! Unit tests for the CommitmentMarketplace Soroban contract.
+//!
+//! ## Coverage
+//! - Initialization, listing, offers, auctions, and reentrancy guard.
+//! - Edge cases and error conditions.
+//!
+//! ## Security
+//! - Explicit tests for reentrancy guard on all entry points.
+//! - All state-changing entry points require authentication.
+//!
+//! ## Usage
+//! Run with `cargo test -p commitment-marketplace` from the workspace root.
+
#![cfg(test)]
extern crate std;
@@ -13,6 +29,9 @@ use soroban_sdk::{
// Test Setup Helpers
// ============================================================================
+/// @notice Helper to deploy and initialize the marketplace contract for tests.
+/// @param e Test environment.
+/// @return (admin, fee_recipient, client)
fn setup_marketplace(e: &Env) -> (Address, Address, CommitmentMarketplaceClient<'_>) {
let admin = Address::generate(e);
let nft_contract = Address::generate(e);
@@ -27,6 +46,9 @@ fn setup_marketplace(e: &Env) -> (Address, Address, CommitmentMarketplaceClient<
(admin, fee_recipient, client)
}
+/// @notice Helper to generate a test token address.
+/// @param e Test environment.
+/// @return Address of a generated token.
fn setup_test_token(e: &Env) -> Address {
// In a real implementation, you'd deploy a token contract
// For testing, we'll use a generated address
@@ -538,6 +560,131 @@ fn test_reentrancy_protection() {
// Benchmark Placeholder Tests
// ============================================================================
+// ============================================================================
+// Reentrancy Guard Unit Tests (Explicit)
+// ============================================================================
+
+/// @notice Test: list_nft fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_list_nft_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.list_nft(&seller, &1, &1000, &payment_token);
+}
+
+/// @notice Test: cancel_listing fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_cancel_listing_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ let token_id = 1u32;
+ client.list_nft(&seller, &token_id, &1000, &payment_token);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.cancel_listing(&seller, &token_id);
+}
+
+/// @notice Test: buy_nft fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_buy_nft_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let buyer = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ let token_id = 1u32;
+ client.list_nft(&seller, &token_id, &1000, &payment_token);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.buy_nft(&buyer, &token_id);
+}
+
+/// @notice Test: make_offer fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_make_offer_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let offerer = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.make_offer(&offerer, &1, &500, &payment_token);
+}
+
+/// @notice Test: accept_offer fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_accept_offer_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let offerer = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ let token_id = 1u32;
+ client.list_nft(&seller, &token_id, &1000, &payment_token);
+ client.make_offer(&offerer, &token_id, &500, &payment_token);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.accept_offer(&seller, &token_id, &offerer);
+}
+
+/// @notice Test: start_auction fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_start_auction_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.start_auction(&seller, &1, &1000, &86400, &payment_token);
+}
+
+/// @notice Test: place_bid fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_place_bid_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let bidder = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ let token_id = 1u32;
+ client.start_auction(&seller, &token_id, &1000, &86400, &payment_token);
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.place_bid(&bidder, &token_id, &1200);
+}
+
+/// @notice Test: end_auction fails if reentrancy guard is set.
+#[test]
+#[should_panic(expected = "Error(Contract, #20)")] // ReentrancyDetected
+fn test_end_auction_reentrancy_guard() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let (_, _, client) = setup_marketplace(&e);
+ let seller = Address::generate(&e);
+ let payment_token = setup_test_token(&e);
+ let token_id = 1u32;
+ client.start_auction(&seller, &token_id, &1000, &1, &payment_token);
+ e.ledger().with_mut(|li| {
+ li.timestamp = 2;
+ });
+ e.storage().instance().set(&DataKey::ReentrancyGuard, &true);
+ client.end_auction(&token_id);
+}
+
#[test]
fn test_gas_listing_operations() {
let e = Env::default();
diff --git a/contracts/price_oracle/src/lib.rs b/contracts/price_oracle/src/lib.rs
index 85fe1f77..fed08803 100644
--- a/contracts/price_oracle/src/lib.rs
+++ b/contracts/price_oracle/src/lib.rs
@@ -1,21 +1,21 @@
#![no_std]
-//! Price Oracle contract for CommitLabs.
+//! # Price Oracle contract for CommitLabs
//!
+//! ## Summary
//! Provides whitelisted price feeds with validation, time-based validity (staleness),
//! and optional fallback. Used for value calculation, drawdown, compliance, and fees.
//!
-//! # Manipulation resistance assumptions
-//! This contract is intentionally a push-based oracle registry, not an on-chain price
-//! discovery mechanism. It assumes:
-//! - oracle addresses added by the admin are trusted to publish honest prices
-//! - consumers enforce freshness through `get_price_valid` and `max_staleness_seconds`
-//! - a single whitelisted oracle update can replace the latest value for an asset
-//! - there is no medianization, TWAP, quorum, or cross-source reconciliation on-chain
+//! ## Manipulation Resistance Assumptions
+//! This contract is a push-based oracle registry. It assumes:
+//! - Oracle addresses added by the admin are trusted to publish honest prices
+//! - Consumers enforce freshness through `get_price_valid` and `max_staleness_seconds`
+//! - A single whitelisted oracle update can replace the latest value for an asset
+//! - There is no medianization, TWAP, quorum, or cross-source reconciliation on-chain
//!
//! Integrators should only use this contract when those trust assumptions are acceptable
-//! for their asset and risk model. See the repository threat model:
-//! [`docs/THREAT_MODEL.md#price-oracle-manipulation-resistance-assumptions`](../../../docs/THREAT_MODEL.md#price-oracle-manipulation-resistance-assumptions).
+//! for their asset and risk model. See:
+//! [`docs/THREAT_MODEL.md#price-oracle-manipulation-resistance-assumptions`](../../../docs/THREAT_MODEL.md#price-oracle-manipulation-resistance-assumptions)
use shared_utils::Validation;
use soroban_sdk::{
@@ -83,13 +83,6 @@ fn read_admin(e: &Env) -> Address {
.unwrap_or_else(|| panic!("Contract not initialized"))
}
-fn require_admin(e: &Env, caller: &Address) {
- caller.require_auth();
- let admin = read_admin(e);
- if *caller != admin {
- panic!("Unauthorized: admin only");
- }
-}
fn is_whitelisted(e: &Env, addr: &Address) -> bool {
e.storage()
@@ -202,32 +195,45 @@ impl PriceOracleContract {
Ok(())
}
- /// Add an address to the oracle whitelist (can push prices). Admin only.
- ///
- /// Security: whitelisted addresses are trusted publishers. A compromised oracle key
- /// can replace the latest on-chain price for any asset it updates.
+ /// @notice Add an address to the oracle whitelist (can push prices).
+ /// @dev Admin only. Whitelisted addresses are trusted publishers.
+ /// @param e Contract environment.
+ /// @param caller Must be the admin.
+ /// @param oracle_address Address to add to the whitelist.
+ /// @return Ok(()) on success, Err(Unauthorized) if not admin.
+ /// @security Whitelisted oracles can overwrite the latest price for any asset. A compromised oracle key can replace the latest on-chain price for any asset it updates.
pub fn add_oracle(e: Env, caller: Address, oracle_address: Address) -> Result<(), OracleError> {
- require_admin(&e, &caller);
+ require_admin_result(&e, &caller)?;
e.storage()
.instance()
.set(&DataKey::OracleWhitelist(oracle_address), &true);
Ok(())
}
- /// Remove an address from the whitelist. Admin only.
+ /// @notice Remove an address from the oracle whitelist.
+ /// @dev Admin only.
+ /// @param e Contract environment.
+ /// @param caller Must be the admin.
+ /// @param oracle_address Address to remove from the whitelist.
+ /// @return Ok(()) on success, Err(Unauthorized) if not admin.
+ /// @security Prevents further updates from the removed address.
pub fn remove_oracle(
e: Env,
caller: Address,
oracle_address: Address,
) -> Result<(), OracleError> {
- require_admin(&e, &caller);
+ require_admin_result(&e, &caller)?;
e.storage()
.instance()
.remove(&DataKey::OracleWhitelist(oracle_address));
Ok(())
}
- /// Check if an address is whitelisted.
+ /// @notice Check if an address is whitelisted as an oracle.
+ /// @dev View function.
+ /// @param e Contract environment.
+ /// @param address Address to check.
+ /// @return True if whitelisted, false otherwise.
pub fn is_oracle_whitelisted(e: Env, address: Address) -> bool {
is_whitelisted(&e, &address)
}
@@ -309,12 +315,14 @@ impl PriceOracleContract {
Ok(data)
}
- /// Set default max staleness (seconds). Admin only.
- ///
- /// Lower values reduce the window in which stale or delayed updates are accepted,
- /// but increase the chance of rejecting otherwise usable data during oracle outages.
+ /// @notice Set default max staleness (seconds).
+ /// @dev Admin only. Lower values reduce the window in which stale or delayed updates are accepted, but increase the chance of rejecting otherwise usable data during oracle outages.
+ /// @param e Contract environment.
+ /// @param caller Must be the admin.
+ /// @param seconds New staleness window in seconds.
+ /// @return Ok(()) on success, Err(Unauthorized) if not admin.
pub fn set_max_staleness(e: Env, caller: Address, seconds: u64) -> Result<(), OracleError> {
- require_admin(&e, &caller);
+ require_admin_result(&e, &caller)?;
set_max_staleness_internal(&e, seconds);
Ok(())
}
@@ -324,7 +332,10 @@ impl PriceOracleContract {
read_config(&e).max_staleness_seconds
}
- /// Get admin address.
+ /// @notice Get admin address.
+ /// @dev View function.
+ /// @param e Contract environment.
+ /// @return Address of the current admin.
pub fn get_admin(e: Env) -> Address {
read_admin(&e)
}
@@ -334,9 +345,13 @@ impl PriceOracleContract {
read_version(&e)
}
- /// Update admin (admin-only).
- ///
- /// Transfers control over whitelist management and configuration.
+ /// @notice Update admin address (admin-only).
+ /// @dev Transfers control over whitelist management and configuration.
+ /// @param e Contract environment.
+ /// @param caller Must be the current admin.
+ /// @param new_admin Address to set as new admin.
+ /// @return Ok(()) on success, Err(Unauthorized) if not admin.
+ /// @security Only the admin can transfer admin authority.
pub fn set_admin(e: Env, caller: Address, new_admin: Address) -> Result<(), OracleError> {
require_admin_result(&e, &caller)?;
e.storage().instance().set(&DataKey::Admin, &new_admin);
diff --git a/contracts/price_oracle/src/tests.rs b/contracts/price_oracle/src/tests.rs
index 85c07416..4918382d 100644
--- a/contracts/price_oracle/src/tests.rs
+++ b/contracts/price_oracle/src/tests.rs
@@ -1,4 +1,68 @@
-#![cfg(test)]
+#[test]
+fn test_admin_only_add_remove_oracle() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let admin = Address::generate(&e);
+ let not_admin = Address::generate(&e);
+ let oracle1 = Address::generate(&e);
+ let oracle2 = Address::generate(&e);
+ let contract_id = e.register_contract(None, PriceOracleContract);
+ let client = PriceOracleContractClient::new(&e, &contract_id);
+
+ e.as_contract(&contract_id, || {
+ PriceOracleContract::initialize(e.clone(), admin.clone()).unwrap();
+ });
+
+ // Only admin can add
+ assert_eq!(client.try_add_oracle(¬_admin, &oracle1), Err(Ok(OracleError::Unauthorized)));
+ assert_eq!(client.try_add_oracle(&admin, &oracle1), Ok(Ok(())));
+ assert!(client.is_oracle_whitelisted(&oracle1));
+
+ // Only admin can remove
+ assert_eq!(client.try_remove_oracle(¬_admin, &oracle1), Err(Ok(OracleError::Unauthorized)));
+ assert_eq!(client.try_remove_oracle(&admin, &oracle1), Ok(Ok(())));
+ assert!(!client.is_oracle_whitelisted(&oracle1));
+
+ // Oracle rotation: remove old, add new
+ assert_eq!(client.try_add_oracle(&admin, &oracle1), Ok(Ok(())));
+ assert_eq!(client.try_add_oracle(&admin, &oracle2), Ok(Ok(())));
+ assert!(client.is_oracle_whitelisted(&oracle1));
+ assert!(client.is_oracle_whitelisted(&oracle2));
+ assert_eq!(client.try_remove_oracle(&admin, &oracle1), Ok(Ok(())));
+ assert!(!client.is_oracle_whitelisted(&oracle1));
+ assert!(client.is_oracle_whitelisted(&oracle2));
+}
+
+#[test]
+fn test_admin_transfer_and_oracle_control() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let admin1 = Address::generate(&e);
+ let admin2 = Address::generate(&e);
+ let oracle = Address::generate(&e);
+ let contract_id = e.register_contract(None, PriceOracleContract);
+ let client = PriceOracleContractClient::new(&e, &contract_id);
+
+ e.as_contract(&contract_id, || {
+ PriceOracleContract::initialize(e.clone(), admin1.clone()).unwrap();
+ });
+
+ // Only admin1 can add
+ assert_eq!(client.try_add_oracle(&admin2, &oracle), Err(Ok(OracleError::Unauthorized)));
+ assert_eq!(client.try_add_oracle(&admin1, &oracle), Ok(Ok(())));
+ assert!(client.is_oracle_whitelisted(&oracle));
+
+ // Transfer admin
+ assert_eq!(client.try_set_admin(&admin2, &admin2), Err(Ok(OracleError::Unauthorized)));
+ assert_eq!(client.try_set_admin(&admin1, &admin2), Ok(Ok(())));
+ assert_eq!(client.get_admin(), admin2);
+
+ // Now only admin2 can remove
+ assert_eq!(client.try_remove_oracle(&admin1, &oracle), Err(Ok(OracleError::Unauthorized)));
+ assert_eq!(client.try_remove_oracle(&admin2, &oracle), Ok(Ok(())));
+ assert!(!client.is_oracle_whitelisted(&oracle));
+}
+
use super::*;
use soroban_sdk::testutils::{Address as _, Ledger};
diff --git a/docs/commitment_marketplace.md b/docs/commitment_marketplace.md
new file mode 100644
index 00000000..7cbb25d0
--- /dev/null
+++ b/docs/commitment_marketplace.md
@@ -0,0 +1,44 @@
+# Commitment Marketplace Contract
+
+This page documents the public entry points, access control, and security notes for the `commitment_marketplace` Soroban contract. It is intended for integrators and auditors.
+
+## Entry Points
+
+| Function | Summary | Access Control | Errors / Security Notes |
+|------------------------|----------------------------------------------|-----------------------|----------------------------------------------------------|
+| initialize | Set admin, NFT contract, fee, fee recipient | Admin require_auth | Fails if already initialized |
+| update_fee | Update marketplace fee | Admin require_auth | Fails if not initialized |
+| list_nft | List NFT for sale | Seller require_auth | Fails if price <= 0, listing exists, or not initialized |
+| cancel_listing | Cancel NFT listing | Seller require_auth | Fails if not found or not seller |
+| buy_nft | Buy NFT from listing | Buyer require_auth | Fails if not found, self-buy, or not initialized |
+| make_offer | Make offer on NFT | Offerer require_auth | Fails if amount <= 0 or duplicate offer |
+| accept_offer | Accept offer on NFT | Seller require_auth | Fails if offer not found or not initialized |
+| cancel_offer | Cancel offer | Offerer require_auth | Fails if offer not found |
+| start_auction | Start auction for NFT | Seller require_auth | Fails if price/duration invalid or auction exists |
+| place_bid | Place bid on auction | Bidder require_auth | Fails if bid too low, ended, or self-bid |
+| end_auction | End auction and settle | Anyone (time-gated) | Fails if not ended, already ended, or not found |
+| get_listing | Get listing details | View | Fails if not found |
+| get_all_listings | Get all active listings | View | |
+| get_offers | Get all offers for NFT | View | |
+| get_auction | Get auction details | View | Fails if not found |
+| get_all_auctions | Get all active auctions | View | |
+
+## Security
+- All state-changing entry points require authentication (`require_auth`) for the relevant actor, except `end_auction` (which is time-gated).
+- Reentrancy guard is enforced on all entry points that mutate state and/or make external calls.
+- Arithmetic is performed using checked math; integer division truncates toward zero.
+- No cross-contract NFT ownership checks or transfers are performed in this implementation (see contract comments).
+- All token transfers use Soroban token interface.
+
+## Reentrancy Guard
+- All mutating entry points set/check/clear a `ReentrancyGuard` storage key.
+- If the guard is set, the contract returns `MarketplaceError::ReentrancyDetected`.
+- See contract and tests for explicit coverage.
+
+## Integrator Notes
+- Integrators should expect deterministic errors for all invalid operations.
+- All public APIs are documented with Rustdoc/NatSpec comments in the contract source.
+- See also: `docs/CONTRACT_FUNCTIONS.md` for cross-contract summary.
+
+## Changelog
+- 2026-03-25: Added explicit reentrancy guard tests and NatSpec documentation. This page created for integrator clarity.
diff --git a/docs/price_oracle/admin_controls.md b/docs/price_oracle/admin_controls.md
new file mode 100644
index 00000000..62c16313
--- /dev/null
+++ b/docs/price_oracle/admin_controls.md
@@ -0,0 +1,35 @@
+# Price Oracle: Admin Controls and Oracle Rotation
+
+This document describes the admin-only controls for managing oracle sources in the `price_oracle` contract, including whitelisting, removal, and rotation of trusted price publishers.
+
+## Overview
+
+The `price_oracle` contract is a trusted-publisher registry. Only addresses whitelisted by the admin can publish prices. The admin is responsible for:
+- Adding new oracle addresses (trusted publishers)
+- Removing or rotating out compromised or obsolete oracles
+- Transferring admin authority when needed
+
+## Functions
+
+| Function | Summary | Access Control | Notes |
+|----------|---------|---------------|-------|
+| `add_oracle(caller, oracle_address)` | Add a trusted price publisher | Admin require_auth | Whitelisted oracle can overwrite the latest price for any asset it updates |
+| `remove_oracle(caller, oracle_address)` | Remove a trusted price publisher | Admin require_auth | Prevents further updates from that address |
+| `set_admin(caller, new_admin)` | Transfer oracle admin authority | Admin require_auth | Transfers control over whitelist and configuration |
+
+## Oracle Rotation
+
+Oracle rotation is performed by removing an old oracle address and adding a new one. Only the admin can perform these actions. This ensures that if an oracle key is compromised or needs to be replaced, the admin can promptly update the whitelist.
+
+### Example: Rotating an Oracle
+1. `remove_oracle(caller, old_oracle_address)`
+2. `add_oracle(caller, new_oracle_address)`
+
+## Security Notes
+- Only the admin can modify the whitelist. Attempts by non-admins will fail with `Unauthorized`.
+- Whitelisted oracles are trusted to publish honest prices. Compromised oracles can overwrite the latest price for any asset.
+- Downstream contracts should always use `get_price_valid` and set appropriate staleness windows.
+
+## See Also
+- [CONTRACT_FUNCTIONS.md](../CONTRACT_FUNCTIONS.md#price_oracle)
+- [THREAT_MODEL.md](../THREAT_MODEL.md#price-oracle-manipulation-resistance-assumptions)