Skip to content

LootMarket

A fixed-price, signature-based marketplace. Sellers sign orders off-chain; buyers submit them on-chain. Address and explorer link: Deployments.

There is no escrow. The contract never holds a token or a seller's funds. A listed token stays in the seller's wallet and moves directly to the buyer at the moment of the fill. See Marketplace for the design rationale.

The Order struct

solidity
struct Order {
    address maker;       // seller; must sign, and must own the token at fill time
    address collection;  // must be allow-listed
    uint256 tokenId;
    address currency;    // address(0) = native ETH
    uint256 price;       // wei, or the ERC-20's smallest unit
    uint256 expiry;      // unix seconds; fill reverts once block.timestamp > expiry
    uint256 nonce;       // per-maker; single-use
}

Field order is consensus-critical — it is baked into the type hash:

Order(address maker,address collection,uint256 tokenId,address currency,uint256 price,uint256 expiry,uint256 nonce)

EIP-712 domain: name: "LootMarket", version: "1", plus the runtime chain id and this contract's address. Binding both means a signature cannot be replayed on another chain or against another contract.

fulfillOrder

solidity
function fulfillOrder(Order calldata order, bytes calldata signature)
    external payable;

nonReentrant and whenNotPaused. Checks run in this order — useful when debugging a revert, because the first failing check is the one you get:

  1. allowedCollection[order.collection]CollectionNotAllowed
  2. currency is native, or allowedCurrency[...]CurrencyNotAllowed
  3. block.timestamp <= order.expiryOrderExpired
  4. !usedOrCancelled[maker][nonce]NonceUsedOrCancelled
  5. nonce >= minimumNonce[maker]NonceBelowMinimum
  6. signature recovers to order.makerInvalidSignature
  7. collection.ownerOf(tokenId) == order.makerMakerNotOwner
  8. market is approved (per-token or operator) → MarketNotApproved

Then, in strict checks-effects-interactions order:

mark nonce used            ← effect, BEFORE any external call
pay royalty receiver
pay platform fee recipient
pay seller
transfer the NFT           ← interaction, LAST
emit OrderFulfilled

Approval can be either kind

The contract accepts a per-token approve(market, tokenId) or operator approval via setApprovalForAll(market, true). A preflight that only checks the operator flag will wrongly reject a perfectly fillable order.

Paying in native ETH

Send msg.value >= order.price. Excess is refunded in the same transaction; a failed refund reverts with RefundFailed.

Paying in an ERC-20

msg.value must be zero (else NativeNotAccepted), and the buyer must have approved LootMarket for the full price — the contract pulls up to three separate transferFrom calls (royalty, fee, seller).

No ERC-20 currency is allow-listed today

allowedCurrency is empty, so every fill is native ETH. The ERC-20 path exists in the contract but is not reachable until the owner allow-lists a currency.

Cancelling

solidity
function cancel(uint256 nonce) external;              // kills one order
function setMinimumNonce(uint256 minNonce) external;  // kills every order below minNonce

cancel marks a single nonce used. setMinimumNonce is a bulk invalidation watermark — one transaction that invalidates every outstanding order of yours with a lower nonce.

setMinimumNonce is indiscriminate

It invalidates all your orders below the watermark, including ones you meant to keep. There is no way to exempt an order or to lower the watermark again.

Views

solidity
function usedOrCancelled(address maker, uint256 nonce) external view returns (bool);
function minimumNonce(address maker) external view returns (uint256);
function allowedCollection(address) external view returns (bool);
function allowedCurrency(address) external view returns (bool);
function platformFeeBps() external view returns (uint16);
function feeRecipient() external view returns (address);
function paused() external view returns (bool);
function hashOrder(Order calldata order) external view returns (bytes32);
function domainSeparator() external view returns (bytes32);

The mapping getters are singular

allowedCollection and allowedCurrency — not the plural forms. Calling a plural name produces a revert with no matching function, which is easy to misread as a contract problem.

hashOrder and domainSeparator let you verify a signature you produced off-chain before asking a user to rely on it.

Events

solidity
event OrderFulfilled(
    bytes32 indexed orderHash,
    address indexed maker,
    address indexed taker,
    address collection,
    uint256 tokenId,
    address currency,
    uint256 price,
    uint256 royaltyAmount,
    uint256 platformFee
);
event OrderCancelled(address indexed maker, uint256 indexed nonce);
event MinimumNonceUpdated(address indexed maker, uint256 minimumNonce);
event CurrencyAllowed(address indexed currency, bool allowed);
event CollectionAllowed(address indexed collection, bool allowed);
event PlatformFeeUpdated(uint16 bps);
event FeeRecipientUpdated(address indexed recipient);

OrderFulfilled breaks out royaltyAmount and platformFee separately, so an indexer can compute exact seller proceeds without re-reading contract state at that block.

Errors

ErrorCause
CollectionNotAllowed(address)Collection is not allow-listed
CurrencyNotAllowed(address)Currency is not allow-listed
OrderExpired()block.timestamp > order.expiry
NonceUsedOrCancelled()Already filled, or cancelled
NonceBelowMinimum()Bulk-cancelled by setMinimumNonce
InvalidSignature()Signature does not recover to order.maker
MakerNotOwner()Seller no longer owns the token
MarketNotApproved()Neither per-token nor operator approval is set
IncorrectPayment(uint256 expected, uint256 provided)Native value below price
NativeNotAccepted()Sent ETH on an ERC-20 order
FeeTooHigh()Attempted platform fee above 10%
FeeExceedsPrice()Royalty + fee would exceed the price
RefundFailed() / PaymentFailed()A value transfer failed
SelfPurchase()Buyer is the maker — filling your own order
ZeroAddress()An address argument was zero

SelfPurchase is active

Filling your own order reverts. Before the 2026-08-11 upgrade it succeeded — the price returned to the maker while the royalty and platform fee did not, which made it a fixed-cost way to manufacture volume. It is now blocked in the contract. See the changelog.

Royalty lookup is defensive

The contract queries ERC-2981 on the collection. A collection that does not implement it, or that returns a zero receiver, produces zero royalty rather than reverting — a non-compliant collection cannot brick fills. If royalty plus platform fee ever exceeded the price, the fill reverts with FeeExceedsPrice rather than underflowing the seller's proceeds.

Signature support

Signatures are verified with ECDSA.recover, so EOA signatures only. ERC-1271 smart-contract wallets (Safe, most AA wallets) cannot currently be makers — their signatures will not recover to order.maker and the fill reverts with InvalidSignature.

Owner functions

setCurrencyAllowed, setCollectionAllowed, setPlatformFee, setFeeRecipient, pause, unpause, and UUPS upgrade authorization. See Admin & upgrades.

LootFi is not affiliated with, endorsed by, or sponsored by Valve Corporation. Counter-Strike and Steam are trademarks of Valve Corporation.