Auctionhouse Contracts Capabilities
1. Introduction
The Auctionhouse Contracts is a comprehensive marketplace system for selling NFTs and other digital assets. It is a fork of the Manifold Gallery Auctionhouse contracts, written for the Cryptoart channel on Farcaster.
Key Differences from Original
- Membership-Based Seller Registry: The seller registry is linked to active hypersub membership (STP v2 NFT's
balanceOffunction returns time-remaining)
Architecture Overview
The system is built with a modular architecture:
- MarketplaceCore: Core abstract contract containing all marketplace logic
- MarketplaceUpgradeable: Upgradeable implementation using UUPS proxy pattern
- MarketplaceLib: Library containing listing construction and validation logic
- SettlementLib: Library handling all payment and settlement logic
- TokenLib: Library providing token transfer utilities for ERC721 and ERC1155
This architecture allows for:
- Upgradeable contracts for bug fixes and feature additions
- Reusable library code
- Clear separation of concerns
2. Listing Types
The auctionhouse supports four distinct listing types, each designed for different sales scenarios:
2.1 INDIVIDUAL_AUCTION
Traditional single-item auctions with competitive bidding.
Characteristics:
- One token per listing (
totalAvailablemust equaltotalPerSale) - Supports ERC721 and ERC1155 tokens (non-lazy only)
- Buyers place bids instead of direct purchases
- Highest bidder wins when auction ends
- Can optionally accept offers (if enabled and no bids have been placed)
Configuration Options:
initialAmount: Reserve price (minimum bid)minIncrementBPS: Minimum bid increase percentage (basis points)extensionInterval: If a bid is placed within this many seconds before auction end, extend auction by this durationstartTime: Auction start time (0 = starts on first bid)endTime: Auction end time (if startTime is 0, represents duration from first bid)
Lifecycle:
- Listing created with token in escrow
- Bidders place bids (must meet minimum increment)
- Auction extends if bid placed within
extensionIntervalof end - Seller or buyer calls
finalize()after auction ends - Highest bidder receives token, seller receives payment
Constraints:
- Cannot be lazy minted
- Must have
totalAvailable == totalPerSale - Cannot accept offers once a bid has been placed
2.2 FIXED_PRICE
Direct purchase listings at a set price. Buyers can purchase immediately without bidding.
Characteristics:
- Supports multiple purchases (multi-edition)
- Supports ERC721 and ERC1155 tokens (non-lazy only)
- Immediate purchase at fixed price
- Multiple buyers can purchase until supply runs out
Configuration Options:
initialAmount: Fixed purchase pricetotalAvailable: Total number of tokens availabletotalPerSale: Number of tokens per purchasestartTime: Sale start time (0 = starts on first purchase)endTime: Sale end time (if startTime is 0, represents duration from first purchase)
Lifecycle:
- Listing created with tokens in escrow
- Buyers call
purchase()with exact amount - Tokens transferred immediately on purchase
- Listing auto-finalizes when all tokens sold
Constraints:
- Cannot be lazy minted
- Cannot use
extensionIntervalorminIncrementBPS - Cannot have delivery fees
2.3 DYNAMIC_PRICE
Price changes based on sales progress. Perfect for bonding curves, Dutch auctions, or time-based pricing.
Characteristics:
- Price determined by
IPriceEnginecontract - Supports lazy minting only
- Price adjusts based on
alreadyMintedcount - Enables sophisticated pricing mechanisms
Configuration Options:
initialAmount: Must be 0 (price comes from IPriceEngine)totalAvailable: Total supply availabletotalPerSale: Typically 1 (required for lazy mints)- Token contract must implement
IPriceEngine - No reserve price or increment requirements
Price Engine Interface:
function price(uint256 assetId, uint256 alreadyMinted, uint24 count) view external returns (uint256);
Lifecycle:
- Listing created with lazy delivery configured
- Buyers call
purchase()with payment (may send excess, refunded) IPriceEngine.price()called to determine current price- Token lazy-minted via
ILazyDelivery.deliver() - Payment settled at calculated price
Constraints:
- Must be lazy minted
- Requires
IPriceEngineimplementation initialAmountmust be 0- Cannot use
extensionIntervalorminIncrementBPS - Cannot have delivery fees
2.4 OFFERS_ONLY
Listing where sellers accept offers from buyers. No direct purchase option.
Characteristics:
- Buyers make offers
- Seller chooses which offers to accept
- Supports multiple offers simultaneously
- Offers can be rescinded by buyers (with restrictions)
Configuration Options:
initialAmount: Must be 0startTime: Must be in the futureendTime: Offer acceptance deadline- Buyers make offers, seller accepts via
accept()
Lifecycle:
- Listing created (token can be lazy or non-lazy)
- Buyers call
offer()with their offer amount - Funds held in escrow
- Seller calls
accept()to accept specific offers - Accepted offers: tokens delivered, payment settled
- Unaccepted offers can be rescinded after listing ends + 24 hours
Offer Rescinding Rules:
- For OFFERS_ONLY listings: Can only rescind after listing finalized OR 24 hours after end time
- For auction listings with offers enabled: Can rescind anytime before bid is placed
Constraints:
initialAmountmust be 0startTimemust be in the future- Cannot use
extensionIntervalorminIncrementBPS - Cannot have delivery fees
3. Token Support
3.1 ERC721 Support
Single, unique tokens (1/1 NFTs).
Usage:
INDIVIDUAL_AUCTION: One ERC721 token per auctionFIXED_PRICE: Can sell multiple copies if contract supports multiple token IDstotalPerSale: Must be 1- Tokens transferred via
transferFrom()
Intake Mechanism:
- Token must be approved and transferred to marketplace contract
- Marketplace holds token in escrow until sale completes
3.2 ERC1155 Support
Multi-edition tokens (editions/multiples).
Usage:
FIXED_PRICE: Can sell editions (e.g., 100 copies)totalAvailable: Total supply of the editiontotalPerSale: Number of editions per purchase (e.g., 1, 5, 10)- Tokens transferred via
safeTransferFrom()
Example: Selling 100 copies where each purchase gives 1 copy:
totalAvailable: 100totalPerSale: 1- Buyers can purchase until all 100 are sold
Intake Mechanism:
- Entire
totalAvailableamount must be transferred to marketplace initially - Marketplace distributes tokens per purchase
3.3 Lazy Minting Support
Tokens minted on-demand at purchase time via ILazyDelivery interface.
When to Use:
DYNAMIC_PRICE: Required for dynamic pricingOFFERS_ONLY: Can use lazy minting- Any listing where you want to mint at purchase time
LazyDelivery Interface:
function deliver(
uint40 listingId,
address to,
uint256 assetId,
uint24 payableCount,
uint256 payableAmount,
address payableERC20,
uint256 index
) external;
Requirements:
- Token contract must implement
ILazyDelivery totalPerSalemust be 1- Seller never transfers token upfront
- Token minted when
deliver()is called
Benefits:
- No upfront token transfer
- Enables dynamic pricing
- Lower gas costs for sellers
- Supports on-demand minting with attributes
4. Payment Options
4.1 Native ETH Payments
Default payment method using native blockchain currency.
Usage:
- Set
erc20field toaddress(0)inListingDetails - Buyers send ETH with
purchase(),bid(), oroffer()calls - Exact amount validation (except DYNAMIC_PRICE which allows excess)
Payment Flow:
- Buyer sends ETH with transaction
- Contract validates amount matches listing price
- ETH held in contract escrow
- Settlement distributes ETH to seller and fee recipients
4.2 ERC20 Token Payments
Alternative payment using any ERC20 token.
Usage:
- Set
erc20field to token contract address inListingDetails - Buyers must approve marketplace contract first
- Contract uses
transferFrom()to collect payment
Supported Tokens:
- Any ERC20-compliant token
- USDC, USDT, DAI, etc.
- Custom tokens
Payment Flow:
- Buyer approves marketplace contract
- Buyer calls
purchase(),bid(), oroffer()withmsg.value = 0 - Contract calls
transferFrom()to collect tokens - Settlement distributes tokens to seller and fee recipients
4.3 Payment Validation and Settlement
Validation:
- FIXED_PRICE: Exact amount required
- INDIVIDUAL_AUCTION: Bid must meet minimum increment
- DYNAMIC_PRICE: Allows excess payment (refunded)
- OFFERS_ONLY: Any amount accepted
Settlement Distribution:
- Marketplace fee (if configured)
- Referrer fee (if referrer provided)
- Royalties (if seller is not token creator)
- Seller/receivers (remaining proceeds)
5. Advanced Features
5.1 Bidding System
Comprehensive auction bidding with multiple features.
Reserve Prices:
initialAmountserves as minimum bid/reserve price- First bid must meet or exceed reserve
- If no bids meet reserve, seller can cancel
Minimum Increments:
minIncrementBPS: Percentage-based minimum bid increase- Example: 500 BPS = 5% minimum increase
- If 0, minimum increase is 1 wei
Auction Extensions:
extensionInterval: Seconds before end time- If bid placed within extension interval, auction extends
- Prevents last-second sniping
- Extension duration equals
extensionInterval
Bid Refunds:
- Previous bidder automatically refunded when outbid
- Refunds use escrow system if direct transfer fails
- Escrowed funds can be withdrawn via
withdrawEscrow()
5.2 Offers System
Flexible offer mechanism for auctions and OFFERS_ONLY listings.
When Offers Are Available:
OFFERS_ONLYlistings: Always enabledINDIVIDUAL_AUCTION: Enabled ifacceptOffersflag set AND no bids placed
Making Offers:
- Buyers call
offer()with amount - Funds held in escrow
- Multiple offers can exist simultaneously
- Buyers can increase existing offers
Accepting Offers:
- Seller calls
accept()with offer addresses and amounts - Can accept multiple offers at once
- Seller can accept partial amount (via
maxAmountparameter) - Accepted offers: tokens delivered, payments settled
Rescinding Offers:
- Buyers can rescind their own offers
- OFFERS_ONLY: Only after listing finalized OR 24 hours after end time
- Auction offers: Can rescind anytime before bid placed
- Seller can rescind others' offers (after listing ends)
5.3 Multi-Receiver Splits
Distribute sale proceeds to multiple recipients.
Configuration:
ListingReceiver[] receivers = [
{ receiver: address1, receiverBPS: 3000 }, // 30%
{ receiver: address2, receiverBPS: 4000 }, // 40%
{ receiver: address3, receiverBPS: 3000 } // 30%
];
Requirements:
- All receiver BPS must sum to exactly 10000 (100%)
- Can have any number of receivers
- Seller receives portion if not in receivers list
Use Cases:
- Revenue sharing with collaborators
- Split payments to multiple wallets
- Automatic distribution to DAO treasury
- Artist/curator splits
5.4 Referrer System
Optional referral fees to incentivize discovery.
Configuration:
- Set
enableReferrer = truewhen creating listing referrerBPSset globally by marketplace admin- Referrer address passed in purchase/bid/offer calls
Payment Flow:
- Referrer receives percentage of sale price
- Deducted from seller proceeds
- Separate from marketplace fees
Example:
- Sale price: 1 ETH
- Referrer BPS: 200 (2%)
- Referrer receives: 0.02 ETH
- Seller receives: 0.98 ETH (minus marketplace fees)
5.5 Identity Verification
Access control via IIdentityVerifier interface.
Interface:
function verify(
uint40 listingId,
address identity,
address tokenAddress,
uint256 tokenId,
uint24 requestCount,
uint256 requestAmount,
address requestERC20,
bytes calldata data
) external returns (bool);
Use Cases:
- Whitelist-based sales
- KYC/AML compliance
- Token-gated access
- Custom permission logic
Implementation:
- Set
identityVerifieraddress inListingDetails - Called before every purchase, bid, or offer
- Must return
truefor transaction to proceed
5.6 Delivery Fees
Additional fees required to deliver tokens (auctions only).
Configuration:
DeliveryFees {
deliverBPS: 100, // 1% of sale price
deliverFixed: 100000 // 0.0001 ETH fixed fee
}
Usage:
- Only applicable to
INDIVIDUAL_AUCTIONlistings - Buyer pays delivery fee when finalizing auction
- Fee distributed to seller/receivers (not marketplace)
Example:
- Auction price: 1 ETH
- Delivery fee: 1% + 0.0001 ETH
- Buyer pays: 1.01 ETH + 0.0001 ETH = 1.0101 ETH total
5.7 Royalty Support
Automatic royalty distribution via RoyaltyEngineV1.
Integration:
- Marketplace configured with RoyaltyEngineV1 address
- Royalties fetched via
getRoyalty()call - Automatically distributed on sale
Royalty Logic:
- Only paid if seller is NOT token creator
- Royalties deducted from seller proceeds
- Supports multiple royalty recipients
- Handles ERC2981 and other royalty standards
Exclusions:
- Lazy minted tokens: No royalties (minted at sale time)
- Token creator sales: No royalties (creator already owns)
5.8 Seller Registry
Optional seller authorization via IMarketplaceSellerRegistry.
MembershipSellerRegistry:
- Checks if seller holds membership NFT
- Uses
balanceOf()to verify membership - Supports STP v2 NFTs (time-based membership)
Custom Registry:
- Implement
IMarketplaceSellerRegistryinterface - Return
trueinisAuthorized()for approved sellers - Enables custom seller verification logic
Registries:
- None: Anyone can list (if marketplace enabled)
- Membership registry: Only NFT holders can list
- Custom: Implement your own verification
6. Listing Lifecycle
6.1 Creating Listings
Function: createListing()
Parameters:
listingDetails: Listing configuration (type, pricing, timing)tokenDetails: Token to sell (address, ID, spec, lazy flag)deliveryFees: Delivery fee configuration (auctions only)listingReceivers: Revenue split recipients (optional)enableReferrer: Enable referrer feesacceptOffers: Enable offers on auctionsdata: Additional data for seller registry/identity verifier
Process:
- Validate listing configuration
- Check seller authorization (if registry set)
- Transfer tokens to marketplace (if not lazy)
- Create listing record
- Emit
CreateListingevent - Return listing ID
Roles: Seller
6.2 Modifying Listings
Function: modifyListing()
Parameters:
listingId: Listing to modifyinitialAmount: New reserve pricestartTime: New start timeendTime: New end time
Constraints:
- Only seller can modify
- Cannot modify if listing has started/completed
- For auctions: Cannot modify if bid placed
- For fixed price: Cannot modify if any sales occurred
- Dynamic price: Cannot change
initialAmount(must be 0)
Use Cases:
- Extend auction duration
- Adjust reserve price
- Delay start time
Roles: Seller
6.3 Purchasing
Function: purchase()
Overloads:
purchase(listingId)- Purchase 1 itempurchase(listingId, count)- Purchase multiple itemspurchase(referrer, listingId)- With referrerpurchase(referrer, listingId, count)- With referrer and count- All variants support optional
dataparameter
Process:
- Validate listing is purchasable
- Check identity verification (if configured)
- Transfer payment from buyer
- Deliver tokens (direct or lazy)
- Settle payment (distribute proceeds)
- Emit
PurchaseEvent - Auto-finalize if all sold
Roles: Buyer
6.4 Bidding
Function: bid()
Overloads:
bid(listingId, increase)- Place/increase bidbid(listingId, amount, increase)- Bid specific amountbid(referrer, listingId, increase)- With referrerbid(referrer, listingId, amount, increase)- Full options- All variants support optional
dataparameter
Process:
- Validate auction requirements
- Check minimum bid increment
- Refund previous bidder (if any)
- Accept new bid
- Extend auction if needed
- Emit
BidEvent
Roles: Bidder
6.5 Making Offers
Function: offer()
Overloads:
offer(listingId, increase)- Make/increase offeroffer(listingId, amount, increase)- Offer specific amountoffer(referrer, listingId, increase)- With referrer- All variants support optional
dataparameter
Process:
- Validate offers are allowed
- Check identity verification (if configured)
- Transfer offer amount to escrow
- Update or create offer record
- Emit
OfferEvent
Roles: Buyer
6.6 Accepting Offers
Function: accept()
Parameters:
listingId: Listing with offersaddresses: Array of offer addresses to acceptamounts: Expected amounts (for validation)maxAmount: Maximum to accept (for partial acceptance)
Process:
- Validate seller owns listing
- Validate offers exist and match expected amounts
- Finalize listing
- Deliver tokens to offerers
- Settle payments
- Emit
AcceptOfferEventfor each
Roles: Seller
6.7 Rescinding Offers
Function: rescind()
Overloads:
rescind(listingId)- Rescind caller's offerrescind(listingIds[])- Rescind caller's offers on multiple listingsrescind(listingId, addresses[])- Seller rescinds others' offers
Constraints:
- OFFERS_ONLY: Only after finalized OR 24 hours after end
- Auction offers: Anytime before bid placed
- Seller can rescind others' offers after listing ends
Process:
- Validate rescind conditions
- Remove offer from storage
- Refund offer amount
- Emit
RescindOfferEvent
Roles: Buyer or Seller
6.8 Finalizing Auctions
Function: finalize()
Process:
- Validate auction has ended
- Mark listing as finalized
- If no bids: Return token to seller (if not lazy)
- If bid exists: Deliver token to highest bidder, settle payment
- Emit
FinalizeListingevent
Roles: Seller or Buyer
Note: For auctions with tokens in escrow, seller can call collect() to get paid before finalizing.
6.9 Collecting Proceeds
Function: collect()
Process:
- Validate auction has ended
- Validate token is in escrow (not lazy)
- Validate bid exists and not settled
- Settle bid payment to seller
- Token remains in escrow until
finalize()
Use Cases:
- Seller wants payment before delivering token
- Split payment and delivery transactions
Roles: Seller
6.10 Canceling Listings
Function: cancel()
Parameters:
listingId: Listing to cancelholdbackBPS: Percentage to holdback (admin only, max 10%)
Constraints:
- Seller: Can cancel if no bids/sales and no holdback
- Admin: Can cancel anytime with optional holdback
Process:
- Validate cancellation allowed
- End and finalize listing
- Refund bids (with holdback if admin)
- Return unsold tokens to seller
- Emit
CancelListingevent
Roles: Seller or Admin
7. Configuration & Administration
7.1 Marketplace Fees
Functions:
setFees(marketplaceFeeBPS, marketplaceReferrerBPS)
Configuration:
marketplaceFeeBPS: Marketplace fee percentage (max 15%)referrerBPS: Referrer fee percentage (max 15%)
Fee Collection:
- Marketplace fees accumulated in
_feesCollectedmapping - Can withdraw via
withdraw() - Applied to all sales
Roles: Admin
7.2 Marketplace Enable/Disable
Function:
setEnabled(enabled)
Effect:
true: All listings allowedfalse: No new listings, existing listings continue
Roles: Admin
7.3 Seller Registry
Function:
setSellerRegistry(registry)
Registry:
- Must implement
IMarketplaceSellerRegistry address(0): No registry (anyone can list)- Non-zero: Only authorized sellers can list
Roles: Admin
7.4 Royalty Engine
Function:
setRoyaltyEngineV1(royaltyEngineV1)
Configuration:
- Set once (cannot be changed)
- Address of RoyaltyEngineV1 contract
- Used for royalty lookups
Roles: Admin
7.5 Withdrawal Functions
Treasury Withdrawal:
withdraw(amount, receiver)- Withdraw ETHwithdraw(erc20, amount, receiver)- Withdraw ERC20
Escrow Withdrawal:
withdrawEscrow(amount)- Withdraw ETH from escrowwithdrawEscrow(erc20, amount)- Withdraw ERC20 from escrow
Roles:
- Treasury: Admin only
- Escrow: Owner of escrowed funds
8. Key Data Structures
8.1 Listing Struct
struct Listing {
uint40 id;
address payable seller;
bool finalized;
uint24 totalSold;
uint16 marketplaceBPS;
uint16 referrerBPS;
ListingDetails details;
TokenDetails token;
ListingReceiver[] receivers;
DeliveryFees fees;
Bid bid;
bool offersAccepted;
}
Fields:
id: Unique listing identifierseller: Address selling the tokenfinalized: Whether listing is completedtotalSold: Total tokens sold (not number of sales)marketplaceBPS: Marketplace fee percentagereferrerBPS: Referrer fee percentagedetails: Listing configurationtoken: Token being soldreceivers: Revenue split recipientsfees: Delivery fee configurationbid: Current highest bid (auctions only)offersAccepted: Whether offers are enabled
8.2 ListingDetails Struct
struct ListingDetails {
uint256 initialAmount;
ListingType type_;
uint24 totalAvailable;
uint24 totalPerSale;
uint16 extensionInterval;
uint16 minIncrementBPS;
address erc20;
address identityVerifier;
uint48 startTime;
uint48 endTime;
}
Fields:
initialAmount: Reserve price or fixed pricetype_: Listing type enumtotalAvailable: Total tokens availabletotalPerSale: Tokens per purchaseextensionInterval: Auction extension secondsminIncrementBPS: Minimum bid increment percentageerc20: Payment token (0 = ETH)identityVerifier: Access control contractstartTime: Start timestamp (0 = on first action)endTime: End timestamp
8.3 TokenDetails Struct
struct TokenDetails {
uint256 id;
address address_;
TokenLib.Spec spec;
bool lazy;
}
Fields:
id: Token ID or asset IDaddress_: Token contract addressspec: ERC721 or ERC1155lazy: Whether lazy minted
8.4 Bid Struct
struct Bid {
uint256 amount;
address payable bidder;
bool delivered;
bool settled;
bool refunded;
uint48 timestamp;
address payable referrer;
}
Fields:
amount: Bid amountbidder: Bidder addressdelivered: Token delivered flagsettled: Payment settled flagrefunded: Refunded flagtimestamp: Bid timestampreferrer: Referrer address
8.5 Offer Struct
struct Offer {
uint200 amount;
uint48 timestamp;
bool accepted;
address payable referrer;
address erc20;
}
Fields:
amount: Offer amount (max 2^200-1)timestamp: Offer timestampaccepted: Accepted flagreferrer: Referrer addresserc20: Currently unused
8.6 DeliveryFees Struct
struct DeliveryFees {
uint16 deliverBPS;
uint240 deliverFixed;
}
Fields:
deliverBPS: Percentage-based feedeliverFixed: Fixed fee amount
8.7 ListingReceiver Struct
struct ListingReceiver {
address payable receiver;
uint16 receiverBPS;
}
Fields:
receiver: Recipient addressreceiverBPS: Percentage share (must sum to 10000)
9. Integrations & Extensions
9.1 ILazyDelivery Interface
Purpose: Enable lazy minting of tokens at purchase time.
Interface:
interface ILazyDelivery is IERC165 {
function deliver(
uint40 listingId,
address to,
uint256 assetId,
uint24 payableCount,
uint256 payableAmount,
address payableERC20,
uint256 index
) external;
}
Implementation Requirements:
- Token contract must implement this interface
- Called by marketplace when token needs delivery
- Should mint and transfer token to
toaddress - Can use
listingIdfor permissioning
Example Use Cases:
- Mint NFT with purchase
- Assign attributes based on purchase order
- On-demand token creation
9.2 IPriceEngine Interface
Purpose: Determine dynamic pricing based on sales progress.
Interface:
interface IPriceEngine is IERC165 {
function price(
uint256 assetId,
uint256 alreadyMinted,
uint24 count
) view external returns (uint256);
}
Parameters:
assetId: Asset identifieralreadyMinted: Number already soldcount: Number being purchased
Return:
- Price in wei (or token units if ERC20)
Price Models:
- Linear bonding curve
- Exponential curve
- Dutch auction (decreasing)
- Step pricing
Implementation Requirements:
- Must be view function
- Must return price for given parameters
- Should be consistent (same inputs = same output)
9.3 IIdentityVerifier Interface
Purpose: Access control for purchases, bids, and offers.
Interface:
interface IIdentityVerifier is IERC165 {
function verify(
uint40 listingId,
address identity,
address tokenAddress,
uint256 tokenId,
uint24 requestCount,
uint256 requestAmount,
address requestERC20,
bytes calldata data
) external returns (bool);
}
Parameters:
listingId: Listing identifieridentity: Buyer/bidder addresstokenAddress: Token contracttokenId: Token IDrequestCount: Items requestedrequestAmount: Payment amountrequestERC20: Payment tokendata: Additional verification data
Return:
trueif authorized,falseotherwise
Implementation Ideas:
- NFT whitelist checking
- Signature verification
- KYC integration
- Custom logic
9.4 IMarketplaceSellerRegistry Interface
Purpose: Control who can create listings.
Interface:
interface IMarketplaceSellerRegistry is IERC165 {
function isAuthorized(
address seller,
bytes calldata data
) external view returns(bool);
}
Implementation:
MembershipSellerRegistry: Checks NFT balance- Custom registries: Implement your own logic
Use Cases:
- Membership-gated listings
- DAO-only listings
- Verified seller program
9.5 RoyaltyEngineV1 Integration
Purpose: Standard royalty lookup and distribution.
Integration:
- Marketplace calls
getRoyalty()on RoyaltyEngineV1 - Returns recipients and amounts
- Marketplace distributes automatically
Supported Standards:
- ERC2981
- Custom royalty implementations
Royalty Logic:
- Only paid if seller is not token creator
- Deducted from seller proceeds
- Multiple recipients supported
10. Security Features
10.1 Reentrancy Protection
Mechanism:
ReentrancyGuardUpgradeableon critical functionsnonReentrantmodifier on state-changing operations
Protected Functions:
purchase()bid()offer()finalize()cancel()withdraw()
Prevention:
- Prevents recursive calls during state changes
- Ensures atomic operations
10.2 Escrow System
Purpose: Handle failed token transfers gracefully.
Mechanism:
- Failed refunds stored in escrow mapping
- Users can withdraw via
withdrawEscrow() - Prevents permanent fund loss
Use Cases:
- Contract wallets that reject transfers
- Gas limit issues
- Token contract issues
10.3 Admin Controls
Access Control:
AdminControlUpgradeablefor admin functionsOwnableUpgradeablefor ownership- Separate admin and owner roles
Admin Functions:
- Fee configuration
- Marketplace enable/disable
- Seller registry setting
- Treasury withdrawal
- Cancel listings (with holdback)
Owner Functions:
- Admin management
- Ownership transfer
10.4 Access Control Mechanisms
Seller Authorization:
- Seller registry check on listing creation
- Optional membership verification
Buyer Authorization:
- Identity verifier on purchases/bids/offers
- Custom permission logic
Modification Authorization:
- Only seller can modify listing
- Only seller can finalize (unless ended)
- Only seller can accept offers
11. Examples & Use Cases
11.1 Basic 1/1 Auction
Scenario: Artist selling a single NFT via auction.
// Listing details
ListingDetails memory details = ListingDetails({
initialAmount: 0.1 ether, // Reserve: 0.1 ETH
type_: ListingType.INDIVIDUAL_AUCTION,
totalAvailable: 1,
totalPerSale: 1,
extensionInterval: 300, // 5 min extension
minIncrementBPS: 500, // 5% minimum increase
erc20: address(0), // ETH payment
identityVerifier: address(0), // No restrictions
startTime: block.timestamp, // Start now
endTime: block.timestamp + 7 days // 7 day auction
});
TokenDetails memory token = TokenDetails({
id: 1,
address_: nftContract,
spec: TokenLib.Spec.ERC721,
lazy: false
});
// Create listing
uint40 listingId = marketplace.createListing(
details,
token,
DeliveryFees({deliverBPS: 0, deliverFixed: 0}),
new ListingReceiver[](0), // No splits
false, // No referrer
false, // No offers
""
);
11.2 Multi-Edition Fixed Price Sale
Scenario: Selling 100 editions at fixed price.
ListingDetails memory details = ListingDetails({
initialAmount: 0.05 ether, // 0.05 ETH per edition
type_: ListingType.FIXED_PRICE,
totalAvailable: 100, // 100 editions
totalPerSale: 1, // 1 per purchase
extensionInterval: 0, // Not applicable
minIncrementBPS: 0, // Not applicable
erc20: address(0),
identityVerifier: address(0),
startTime: 0, // Start on first purchase
endTime: 30 days // Duration from first purchase
});
TokenDetails memory token = TokenDetails({
id: 1,
address_: editionContract,
spec: TokenLib.Spec.ERC1155,
lazy: false
});
// Transfer 100 editions to marketplace first
ERC1155(editionContract).safeTransferFrom(
seller,
address(marketplace),
1, // tokenId
100, // quantity
""
);
11.3 Dynamic Price Bonding Curve
Scenario: Price increases with each sale.
// PriceEngine contract
contract BondingCurvePriceEngine is IPriceEngine {
function price(uint256 assetId, uint256 alreadyMinted, uint24 count)
external pure override returns (uint256) {
// Linear: price = basePrice + (alreadyMinted * increment)
uint256 basePrice = 0.01 ether;
uint256 increment = 0.001 ether;
return basePrice + (alreadyMinted * increment);
}
}
// Listing details
ListingDetails memory details = ListingDetails({
initialAmount: 0, // Must be 0
type_: ListingType.DYNAMIC_PRICE,
totalAvailable: 1000,
totalPerSale: 1,
extensionInterval: 0,
minIncrementBPS: 0,
erc20: address(0),
identityVerifier: address(0),
startTime: block.timestamp,
endTime: block.timestamp + 30 days
});
TokenDetails memory token = TokenDetails({
id: 1,
address_: lazyMintContract, // Must implement ILazyDelivery
spec: TokenLib.Spec.ERC721,
lazy: true // Required for dynamic price
});
11.4 Offers-Only Listing
Scenario: Seller wants to review offers before accepting.
ListingDetails memory details = ListingDetails({
initialAmount: 0, // Must be 0
type_: ListingType.OFFERS_ONLY,
totalAvailable: 1,
totalPerSale: 1,
extensionInterval: 0,
minIncrementBPS: 0,
erc20: address(0),
identityVerifier: address(0),
startTime: block.timestamp + 1 day, // Must be future
endTime: block.timestamp + 8 days
});
// Buyers make offers
marketplace.offer{value: 0.5 ether}(listingId, false);
marketplace.offer{value: 0.7 ether}(listingId, false);
marketplace.offer{value: 0.6 ether}(listingId, false);
// Seller accepts best offer
address[] memory addresses = new address[](1);
uint256[] memory amounts = new uint256[](1);
addresses[0] = address(0xBuyer2); // 0.7 ETH offer
amounts[0] = 0.7 ether;
marketplace.accept(listingId, addresses, amounts, 0);
11.5 Revenue Split Sale
Scenario: Artist wants to split proceeds with collaborator and DAO.
ListingReceiver[] memory receivers = new ListingReceiver[](3);
receivers[0] = ListingReceiver({
receiver: payable(artist),
receiverBPS: 6000 // 60%
});
receivers[1] = ListingReceiver({
receiver: payable(collaborator),
receiverBPS: 2500 // 25%
});
receivers[2] = ListingReceiver({
receiver: payable(dao),
receiverBPS: 1500 // 15%
});
// Total: 10000 BPS = 100%
marketplace.createListing(
details,
token,
deliveryFees,
receivers, // Revenue split
false,
false,
""
);
11.6 Referrer-Enabled Sale
Scenario: Marketplace wants to reward referrals.
// Enable referrer when creating listing
marketplace.createListing(
details,
token,
deliveryFees,
receivers,
true, // Enable referrer
false,
""
);
// Buyer purchases with referrer
address referrer = address(0xReferrer);
marketplace.purchase{value: price}(referrer, listingId);
// Referrer receives percentage of sale
11.7 Whitelist Sale
Scenario: Only holders of specific NFT can purchase.
// IdentityVerifier contract
contract WhitelistVerifier is IIdentityVerifier {
IERC721 public whitelistNFT;
function verify(
uint40 listingId,
address identity,
address tokenAddress,
uint256 tokenId,
uint24 requestCount,
uint256 requestAmount,
address requestERC20,
bytes calldata data
) external view override returns (bool) {
return whitelistNFT.balanceOf(identity) > 0;
}
}
ListingDetails memory details = ListingDetails({
// ... other fields
identityVerifier: address(whitelistVerifier),
// ...
});
11.8 ERC20 Payment Sale
Scenario: Accepting USDC instead of ETH.
ListingDetails memory details = ListingDetails({
initialAmount: 100 * 10**6, // 100 USDC (6 decimals)
type_: ListingType.FIXED_PRICE,
totalAvailable: 1,
totalPerSale: 1,
extensionInterval: 0,
minIncrementBPS: 0,
erc20: address(usdcContract), // USDC address
identityVerifier: address(0),
startTime: block.timestamp,
endTime: block.timestamp + 7 days
});
// Buyer must approve first
IERC20(usdcContract).approve(address(marketplace), 100 * 10**6);
// Then purchase
marketplace.purchase(listingId);
11.9 Lazy Minting Sale
Scenario: Mint NFT only when purchased.
// LazyDelivery contract
contract LazyMintNFT is ERC721, ILazyDelivery {
function deliver(
uint40 listingId,
address to,
uint256 assetId,
uint24 payableCount,
uint256 payableAmount,
address payableERC20,
uint256 index
) external override {
require(msg.sender == address(marketplace), "Unauthorized");
// Mint with purchase order as attribute
uint256 tokenId = totalSupply() + 1;
_safeMint(to, tokenId);
// Store attributes if needed
}
}
TokenDetails memory token = TokenDetails({
id: 1,
address_: address(lazyMintContract),
spec: TokenLib.Spec.ERC721,
lazy: true // Enable lazy minting
});
11.10 Auction with Offers
Scenario: Allow offers before first bid, then switch to bidding only.
marketplace.createListing(
details,
token,
deliveryFees,
receivers,
false,
true, // Accept offers
""
);
// Buyers can make offers before bidding starts
marketplace.offer{value: 0.5 ether}(listingId, false);
// Once first bid placed, offers disabled
marketplace.bid{value: 0.6 ether}(listingId, false);
// Now only bidding allowed
12. Important Notes
12.1 totalSold vs Number of Sales
totalSold represents the total number of tokens sold, not the number of sales transactions.
Example:
totalAvailable: 100totalPerSale: 5- Each purchase sells 5 tokens
- After 10 purchases:
totalSold = 50 - Number of sales: 10
12.2 Time-Based Constraints
startTime = 0:
- Listing starts on first action (purchase/bid/offer)
endTimebecomes duration from start- Common for "start whenever" listings
startTime > 0:
- Listing starts at specific timestamp
endTimeis absolute end time- Better for scheduled sales
12.3 Lazy vs Non-Lazy Tokens
Non-Lazy (Standard):
- Seller transfers tokens to marketplace upfront
- Tokens held in escrow
- Marketplace transfers on purchase
- Supports ERC721 and ERC1155
Lazy Minting:
- No upfront transfer
- Token minted via
ILazyDelivery.deliver() - Required for
DYNAMIC_PRICElistings totalPerSalemust be 1
12.4 Offer System Differences
Auction Offers:
- Enabled only if
acceptOffersflag set AND no bids - Disabled once first bid placed
- Can rescind anytime before bid
OFFERS_ONLY Listings:
- Offers always enabled
- No direct purchase option
- Can rescind after finalized OR 24 hours after end
12.5 Gas Considerations
Optimizations:
- Lazy minting saves gas (no upfront transfer)
- Batch operations for multiple offers
- ERC20 payments require approval (separate transaction)
Gas Costs:
- Creating listing: Higher for non-lazy (includes transfer)
- Purchase: Standard ERC721/1155 transfer costs
- Bidding: Refund costs if outbid
- Offers: Lower (no token transfer until accepted)
13. Contract Addresses & Deployment
13.1 Main Contracts
- MarketplaceUpgradeable: Upgradeable implementation contract
- MembershipSellerRegistry: NFT-based seller registry
- ILazyDelivery: Interface for lazy minting (implement in your contract)
- IPriceEngine: Interface for dynamic pricing (implement in your contract)
- IIdentityVerifier: Interface for access control (implement in your contract)
13.2 Deployment
Contracts are deployed using Foundry and OpenZeppelin Upgrades:
forge script script/DeployContracts.s.sol:DeployContractsScript \
--rpc-url <RPC_URL> \
--private-key <PRIVATE_KEY> \
--broadcast
Initialize with:
marketplace.initialize(initialOwner);
marketplace.setFees(marketplaceFeeBPS, referrerBPS);
marketplace.setSellerRegistry(sellerRegistryAddress);
marketplace.setRoyaltyEngineV1(royaltyEngineV1Address);
14. Testing
Test suite available in test/MarketplaceUpgradeable.t.sol:
forge test
Coverage includes:
- Basic auction functionality
- Fixed price purchases
- Offer system
- Token transfers
- Fee distribution
15. References
- Manifold Gallery: Original auctionhouse implementation
- OpenZeppelin: Upgradeable contracts and security patterns
- ERC721: NFT standard
- ERC1155: Multi-token standard
- ERC2981: Royalty standard