Skip to content

Functions

DISCLAIMER // NFA // DYOR

This analysis is based on observations of the contract behavior. We are not smart contract security experts. This document aims to explain what the contract appears to do based on the code. It should not be considered a comprehensive security audit or financial advice. Always verify critical information independently and consult with blockchain security professionals for important decisions.

⊙ generated by robots | curated by humans

METADATA
Contract Address 0xb276f62d...1eac1c (etherscan)
Network Ethereum Mainnet
Analysis Date 2026-08-11
Snapshot Block 25733839

Function Selectors

SELECTOR FUNCTION SIGNATURE CATEGORY
0x0793e5fd depositorReclaimBacking(uint256) User
0x0986a5a1 claimTopSpot(uint256) User
0x1b9bc525 treeRootWeight() View
0x1fe543e3 rawFulfillRandomWords(uint256,uint256[]) Keeper
0x20bd63ba acceptBidAsTokens(uint256,uint256) User
0x249d39e9 BPS() View
0x24f74697 callbackGasLimit() View
0x25692962 requestOwnershipHandover() Admin
0x2b0b9641 ownerAcquisitionFeeBps() View
0x2c8bea33 activateListings(uint256) Keeper
0x2fa14f49 configureVrfRequest(bytes32,uint32) Admin
0x34b1670f pendingAcquisitionCount() View
0x3531aed5 acquireBatch(uint256,uint256,uint256) User
0x35390e96 acceptDepositorBid(uint256) User
0x38dc89ef reservedListingCount(uint64) View
0x38f5f005 acquisitionFee() View
0x39c33215 MAX_CALLBACK_GAS_LIMIT() View
0x39ea5e12 acquisitionRefundCredit(address) View
0x3c61c7aa listNFT(address,uint256) User
0x3d21f274 unsettledAcquisitionCount() View
0x3f23b0cb vrfCoordinatorAndSubId() View
0x40ef7ee1 selectionSlippageBps() View
0x41111a4a acquisitions(uint256) View
0x4681a7c6 activeListingCount() View
0x49cfb710 keepNFT(uint256) User
0x4a088a42 ownerSettlementFeeBps() View
0x4c5b2beb payoutFees() Keeper
0x4c68b3b9 feeShareTotal() View
0x548b0de9 acquire(uint256,uint256) User
0x54d1f13d cancelOwnershipHandover() Admin
0x59749e94 vrfService() View
0x59d973db acquisitionEscrowTotal() View
0x5b69ae6a retainedToProtocol() View
0x5b8d02d7 payoutAddress() View
0x5c584c88 feeCredit(address) View
0x61e3c944 setUint(uint256,uint256) Admin
0x6480ef05 acquisitionTokenSlice(uint256) View
0x666cd313 collectionWhitelisted(address) View
0x6a6e8c70 topThresholdBps() View
0x6e658d1a withdrawAcquisitionRefund() User
0x715018a6 renounceOwnership() Admin
0x783c3a09 acquisitionMeta(uint256) View
0x7b9aa10f accruedOwnerFees() View
0x7d4b4a6f relistNFT(uint256) User
0x7f3e2b9c acquire(uint256,uint256,uint256) User
0x823e645a topListingShareBps() View
0x8600e5cb finalizeWindow() View
0x863975fa acquireBatch(uint256,uint256,uint256,uint256) User
0x8ca14105 recoverStuckNFT(uint256) User
0x8da5cb5b owner() View
0x96c82e57 totalWeight() View
0x97cedc76 setBool(uint256,bool) Admin
0x987df4cd quoteAcquisitionPrice() View
0x9e790a76 requestIdAtSequence(uint64) View
0x9eb60921 finalizeUnsettled(uint256) Keeper
0x9ec5a894 rewards() View
0xa2b93478 pendingFees(uint256) View
0xa4e42912 reservedListingAt(uint64,uint256) View
0xa66e8ae2 unfulfilledVrfCount() View
0xaaccf1ec nextListingId() View
0xaec6e273 withdrawListing(uint256) User
0xb3d03582 MIN_CALLBACK_GAS_LIMIT() View
0xb4a7bdf9 settlementWindow() View
0xb5091d48 acquisitionRefundCreditTotal() View
0xb72e9f36 vrfRequestConfig() View
0xb73c6ce9 withdrawEarnings() User
0xb840cf36 claimListingFees(uint256[]) User
0xba20687b topListingPot() View
0xc367b0d2 lastIssuedSequence() View
0xc4c873e6 nextSequenceToProcess() View
0xc5af7460 setCollectionsWhitelisted(address[],bool) Admin
0xc622bfcf updateBacking(uint256,uint256) User
0xd3c4a761 canRescueRewards() View
0xd6eb0dbd weightedBackingTotal() View
0xd7804cb2 reservedForSequence(uint256) View
0xdb9584ce depositorReclaimNFT(uint256) User
0xde74e57b listings(uint256) View
0xdf881bd1 selectionTimeoutBlocks() View
0xe2881eb7 slotToListing(uint256) View
0xea502b3f processAcquisitions(uint256) Keeper
0xead06b1b reconcileUnfulfilledVrfCount() Keeper
0xeba36dbd setAddr(uint256,address) Admin
0xec38a862 setRewards(address) Admin
0xee35bc33 topListingId() View
0xf04e283e completeOwnershipHandover(address) Admin
0xf2fde38b transferOwnership(address) Admin
0xf7375a7f stuckNFTRecipient(uint256) View
0xfa36793f reservedStagedCount() View
0xfb2dd096 settlementDiscountBps() View
0xfc0c546a token() View
0xfee81cf4 ownershipHandoverExpiresAt(address) View
0xff48b8ae vrfServiceFee() View

Summary

CATEGORY COUNT
Total Functions 92
User Functions 18
Keeper Functions 6
Admin Functions 11
View Functions 57

Keeper functions are permissionless but exist to advance protocol state rather than to serve the caller directly. rawFulfillRandomWords is grouped there because only the Chainlink coordinator may call it.


User Functions

Function: listNFT(address collection, uint256 tokenId)

Escrows one ERC-721 together with msg.value as backing, creating a listing. The backing simultaneously sets the listing's inverse selection weight and funds an irrevocable standing bid to buy the NFT back from whoever acquires it.

ATTRIBUTE VALUE
Selector 0x3c61c7aa
Parameters collection — ERC-721 contract; tokenId — token to escrow
Access Public, payable, nonReentrant
Returns listingId
FLAG OBSERVATION
◇ Uses transferFrom, not safeTransferFrom, so a stray safeTransferFrom sent directly to the contract reverts instead of becoming an untracked listing. The depositor must approve first.
☑ Follows the pull with an explicit ownerOf(tokenId) == address(this) check, catching non-standard collections that silently no-op a transfer.
☑ If any acquisition is unresolved, the listing is staged outside the selection tree rather than activated, so it cannot alter a draw that has already been priced and paid for.
△ The contract performs no valuation of the NFT. Backing is whatever the depositor sends; nothing checks it against a floor price or any oracle.
◇ Weight is 1e36 / backing, so a larger backing yields a smaller chance of being drawn.
CONDITION REQUIREMENT
Not in emergency mode withdrawOnly == false
Non-zero backing msg.value != 0
Meets minimum msg.value >= minBacking (0.05 ETH at snapshot)
Real contract collection.code.length != 0
Whitelisted !whitelistEnabled \|\| collectionWhitelisted[collection]
Backing not absurd 1e36 / msg.value != 0
Staging capacity maxStagedListings == 0 \|\| stagedCount < maxStagedListings
STEP ACTION
1 Validate the gauntlet above
2 Compute weight = 1e36 / msg.value
3 Pull the NFT via transferFrom and verify custody
4 Assign listingId = nextListingId++ and write the Listing struct with feeShare = 1
5 If no acquisition is unresolved and staging is empty, activate immediately; otherwise push onto the staging FIFO
6 On activation: ceil the feeDebt checkpoint, allocate a tree slot, add weight to the tree and pool totals, notify the rewards module, and run the top-spot take or seize
VARIABLE CHANGE
nextListingId += 1
listings[listingId] Written
totalWeight, weightedBackingTotal, feeShareTotal, activeListingCount Increased on activation
tree[…], slotToListing[slot] Updated on activation
topListingId, topListingPot May change if the new listing takes or seizes the top
CONDITION REVERT
Emergency mode active WithdrawOnlyActive()
No ETH sent ZeroBacking()
Below minimum BelowMinBacking()
EOA or empty collection InvalidCollection()
Collection not whitelisted CollectionNotWhitelisted()
Backing above 1e36 wei BackingTooHigh()
NFT did not arrive NFTTransferFailed()
Staging queue at cap StagingQueueFull()
function listNFT(address collection, uint256 tokenId) external payable nonReentrant returns (uint256 listingId) {
    listingId = _createListing(collection, tokenId, msg.sender);
}

function _createListing(address collection, uint256 tokenId, address pullFrom)
    internal
    returns (uint256 listingId)
{
    if (withdrawOnly) revert WithdrawOnlyActive();
    if (msg.value == 0) revert ZeroBacking();
    if (msg.value < minBacking) revert BelowMinBacking();
    if (collection.code.length == 0) revert InvalidCollection();
    if (whitelistEnabled && !collectionWhitelisted[collection]) revert CollectionNotWhitelisted();

    uint256 value = msg.value;
    uint256 weight = INVERSE_WEIGHT_NUMERATOR / value;
    if (weight == 0) revert BackingTooHigh();

    if (pullFrom != address(this)) {
        ERC721(collection).transferFrom(pullFrom, address(this), tokenId);
        if (ERC721(collection).ownerOf(tokenId) != address(this)) {
            revert NFTTransferFailed();
        }
    }

    listingId = nextListingId++;
    uint256 feeShare = 1;

    listings[listingId] = Listing({
        collection: collection,
        depositor: msg.sender,
        purchaser: address(0),
        tokenId: tokenId,
        weight: weight,
        value: value,
        feeShare: feeShare,
        feeDebt: 0,
        slot: 0,
        allocatedAt: 0,
        status: ListingStatus.Active
    });

    if (unsettledAcquisitionCount == 0 && stagingHead == 0) {
        _activateListing(listingId);
    } else {
        if (maxStagedListings != 0 && stagedCount >= maxStagedListings) revert StagingQueueFull();
        listings[listingId].status = ListingStatus.Staged;
        _stagingPush(listingId);
        emit ListingStaged(listingId, msg.sender, collection, tokenId, value, block.number);
        if (unsettledAcquisitionCount == 0) _drainStaging(maxActivationsPerAcquisition);
    }
}

Function: acquire(uint256 maxAcquisitionFee, uint256 minWeightedValue)

Pays for and requests one randomized allocation against the current pool. The caller sends at least quoteAcquisitionPrice().total; any excess is refunded in the same transaction. An overload adds a third parameter letting the purchaser choose their own negative-drift tolerance.

ATTRIBUTE VALUE
Selector 0x548b0de9 (two-argument form), 0x7f3e2b9c (three-argument form)
Parameters maxAcquisitionFee — revert if the pool fee exceeds this, 0 disables; minWeightedValue — revert if weightedBackingTotal is below this, 0 disables
Access Public, payable, nonReentrant
Returns requestId from the Chainlink coordinator
FLAG OBSERVATION
☑ Both slippage guards defend against a specific front-run: a deposit that raises the price, or a withdrawal that drains the pool value being paid for.
☑ The sequence number and the exact staged batch to activate are committed before the external VRF call, so a coordinator revert rolls both back atomically.
☑ requestId == 0 or a collision with an existing acquisition reverts DuplicateRequestId(), so a misbehaving coordinator cannot overwrite live state.
△ The VRF service fee is forwarded immediately and is not refunded if the acquisition later expires or is refunded on slippage. Only the pool fee is returned.
△ Both guards default to disabled when passed 0. A purchaser calling with (0, 0) accepts any price and any pool.
◇ The purchaser's tolerances are snapshotted into AcquisitionMeta at request time, so a later owner change to selectionSlippageBps cannot retroactively widen them.
CONDITION REQUIREMENT
Not in emergency mode withdrawOnly == false
Acquisitions open acquisitionsEnabled == true
Non-empty pool totalWeight != 0
Pool value guard minWeightedValue == 0 \|\| weightedBackingTotal >= minWeightedValue
Price guard maxAcquisitionFee == 0 \|\| acquisitionFee() <= maxAcquisitionFee
Payment msg.value >= acquisitionFee() + vrfServiceFee()
STEP ACTION
1 If nothing is unresolved, drain up to maxActivationsPerAcquisition staged listings into the pool
2 Validate preconditions and read the live fee
3 Forward the VRF fee to FWAVRFService.prepareRequests
4 Assign sequence ++lastIssuedSequence and reserve the next staged batch to it
5 Call requestRandomWords on the coordinator with nativePayment: true
6 Write the Acquisition and AcquisitionMeta records, escrow the pool fee, register the reward slice
7 Refund any overpayment
VARIABLE CHANGE
lastIssuedSequence += 1
acquisitions[requestId], acquisitionMeta[requestId], requestIdAtSequence[sequence] Written
pendingAcquisitionCount, unfulfilledVrfCount, unsettledAcquisitionCount += 1 each
acquisitionEscrowTotal += fee
reservedForSequence[…], reservedStagedCount Updated for the reserved batch
CONDITION REVERT
Emergency mode WithdrawOnlyActive()
Still in loading phase AcquisitionsNotEnabled()
Pool empty NoActiveListings()
Pool value drifted below guard PoolValueTooLow()
Fee above guard AcquisitionFeeTooHigh()
Underpayment InsufficientPayment()
Tolerance above 100% InvalidConfig()
Coordinator returned a used or zero id DuplicateRequestId()
function _acquire(uint256 maxAcquisitionFee, uint256 minWeightedValue, uint256 maxNegativeSlippageBps)
    internal
    returns (uint256 requestId)
{
    if (maxNegativeSlippageBps > BPS) revert InvalidConfig();
    if (unsettledAcquisitionCount == 0) _drainStaging(maxActivationsPerAcquisition);

    uint256 fee = _validateAcquisition(maxAcquisitionFee, minWeightedValue);

    uint256 vrf = vrfServiceFee();
    uint256 cost = fee + vrf;
    if (msg.value < cost) revert InsufficientPayment();

    vrfService.prepareRequests{value: vrf}(1);
    emit VrfServiceFeePaid(msg.sender, vrf);

    requestId = _openAcquisition(fee, maxNegativeSlippageBps);

    _refund(msg.value - cost);
}

Function: keepNFT(uint256 listingId)

The purchaser takes the NFT. The listing's backing returns to its depositor less the protocol settlement cut. Available for the entire time the listing is allocated.

ATTRIBUTE VALUE
Selector 0x49cfb710
Parameters listingId — an allocated listing whose purchaser is the caller
Access Purchaser only, nonReentrant
FLAG OBSERVATION
☑ The NFT transfer is strict here: if the collection reverts, the whole call reverts and the purchaser can fall back to acceptDepositorBid rather than be committed to an undeliverable NFT.
☑ Mutually exclusive with acceptDepositorBid by status transition — the listing becomes Settled before any transfer, so a purchaser can never take both the NFT and the ETH.
◇ The depositor receives value × (1 − ownerSettlementFeeBps/10000), currently 99% of their backing.
CONDITION REQUIREMENT
Listing allocated listing.status == ListingStatus.Allocated
Caller is the purchaser listing.purchaser == msg.sender
STEP ACTION
1 Mark the listing Settled
2 Accrue value × ownerSettlementFeeBps / BPS to protocol fees
3 Send the remainder to the depositor
4 safeTransferFrom the NFT to the purchaser, reverting the whole call on failure
CONDITION REVERT
Listing not allocated ListingNotAllocated()
Caller is not the purchaser NotPurchaser()
Collection reverts on transfer Bubbles up from the collection
function keepNFT(uint256 listingId) external nonReentrant {
    Listing storage listing = _allocatedByPurchaser(listingId);

    (address depositor, address purchaser, uint256 toDepositor) = _settleNFTToPurchaser(listingId, listing, true);
    emit NFTKept(listingId, purchaser, depositor, toDepositor);
}

function _payoutBackingToDepositor(Listing storage listing)
    internal
    returns (address depositor, address purchaser, uint256 toDepositor)
{
    listing.status = ListingStatus.Settled;

    depositor = listing.depositor;
    purchaser = listing.purchaser;
    uint256 value = listing.value;

    uint256 ownerCut = value * ownerSettlementFeeBps / BPS;
    toDepositor = value - ownerCut;
    _accrueOwnerFee(ownerCut);

    SafeTransferLib.forceSafeTransferETH(depositor, toDepositor);
}

Function: acceptDepositorBid(uint256 listingId)

The purchaser sells the NFT back into the depositor's standing bid and receives settlementDiscountBps of the backing as ETH — currently 90%. The retained remainder is routed per retainedToProtocol and the NFT goes back to the depositor.

ATTRIBUTE VALUE
Selector 0x35390e96
Parameters listingId
Access Purchaser only, nonReentrant
FLAG OBSERVATION
☑ No separate owner cut is charged on this path — the retained 10% is the protocol's only take, so the purchaser receives the full discount figure.
☑ The NFT return to the depositor is best-effort. A collection that reverts records the depositor against stuckNFTRecipient rather than blocking the purchaser's ETH.
△ retainedToProtocol is true at snapshot, so the retained 10% is protocol revenue rather than being shared with depositors. The owner can flip this at any idle moment.
◇ This is the path that makes the purchaser's pure-ETH expectation negative: pay ~1.025 × expected backing, receive 0.90 × backing.
CONDITION REQUIREMENT
Listing allocated listing.status == ListingStatus.Allocated
Caller is the purchaser listing.purchaser == msg.sender
STEP ACTION
1 Mark the listing Settled
2 Compute payout = value × settlementDiscountBps / BPS and retained = value − payout
3 Route retained to protocol fees or to the depositor distribution per retainedToProtocol
4 Deliver the NFT to the depositor best-effort
5 Send payout to the purchaser
CONDITION REVERT
Listing not allocated ListingNotAllocated()
Caller is not the purchaser NotPurchaser()
function _settleBackingToPurchaser(uint256 listingId, Listing storage listing)
    internal
    returns (address depositor, address purchaser, uint256 payout, uint256 retained)
{
    listing.status = ListingStatus.Settled;

    depositor = listing.depositor;
    purchaser = listing.purchaser;
    uint256 value = listing.value;

    payout = value * settlementDiscountBps / BPS;
    retained = value - payout;

    if (retained != 0) {
        if (retainedToProtocol) _accrueOwnerFee(retained);
        else _distribute(retained);
    }

    _deliverNFT(listingId, listing.collection, depositor, listing.tokenId);
}

Function: withdrawListing(uint256 listingId)

The depositor exits an active listing, recovering the NFT and the full backing, and settling any accrued fee share into withdrawable earnings.

ATTRIBUTE VALUE
Selector 0xaec6e273
Parameters listingId
Access Depositor only, nonReentrant
FLAG OBSERVATION
☑ Backing is returned in full here — the settlement cut applies only when a listing is allocated and resolved, not when it is withdrawn unallocated.
△ Blocked entirely whenever unsettledAcquisitionCount != 0. This is correct (removing weight would steer a paid-for draw) but it means exit is not available on demand while the pool is busy.
☑ Fees are settled before weight removal, so the pending amount is computed against the listing's live share.
◇ If the listing held the top spot, its accrued pot settles to the depositor in the same call.
CONDITION REQUIREMENT
Pool quiescent unsettledAcquisitionCount == 0
Listing active listing.status == ListingStatus.Active
Caller is the depositor listing.depositor == msg.sender
CONDITION REVERT
An acquisition is unresolved WithdrawLocked()
Listing not active ListingNotActive()
Caller is not the depositor NotDepositor()
function withdrawListing(uint256 listingId) external nonReentrant {
    _requireExitUnlocked();

    Listing storage listing = _activeOwned(listingId);

    uint256 value = listing.value;
    address collection = listing.collection;
    uint256 tokenId = listing.tokenId;

    _settleAndRemove(listingId);

    if (topListingId == listingId) _vacateTop();

    listing.status = ListingStatus.Withdrawn;

    SafeTransferLib.forceSafeTransferETH(msg.sender, value);
    ERC721(collection).safeTransferFrom(address(this), msg.sender, tokenId);

    emit ListingWithdrawn(listingId, msg.sender, value);
}

Function: updateBacking(uint256 listingId, uint256 newBacking)

Re-prices an active listing, which also re-prices its inverse selection weight. The depositor sends the shortfall to increase; a decrease refunds the freed backing in the same call.

ATTRIBUTE VALUE
Selector 0xc622bfcf
Parameters listingId; newBacking — the new backing in wei
Access Depositor only, payable, nonReentrant
FLAG OBSERVATION
☑ The exit gate applies in both directions. An increase steers a live draw just as a decrease does, and the code blocks both.
☑ Accrued fees are settled at the old share before the share changes, then the checkpoint is re-ceiled against the new share.
◇ The fee share is a flat 1 either way, so a re-price leaves feeShareTotal unchanged. The delta form is retained defensively.
◇ A holder who reduces their own backing forfeits the top spot; a raise that clears the incumbent by topThresholdBps seizes it.
CONDITION REQUIREMENT
Listing active and owned status == Active, depositor == msg.sender
At or above minimum newBacking >= minBacking
Increases blocked in emergency !(newBacking > oldBacking && withdrawOnly)
Pool quiescent unsettledAcquisitionCount == 0
Sufficient funds msg.value + oldBacking >= newBacking
CONDITION REVERT
Below minimum BelowMinBacking()
Zero ZeroBacking()
Increase during emergency mode WithdrawOnlyActive()
An acquisition is unresolved WithdrawLocked()
Backing above 1e36 wei BackingTooHigh()
Shortfall not covered InsufficientPayment()
uint256 newWeight = INVERSE_WEIGHT_NUMERATOR / newBacking;
if (newWeight == 0) revert BackingTooHigh();

if (msg.value + oldBacking < newBacking) revert InsufficientPayment();
uint256 refund = msg.value + oldBacking - newBacking;

uint256 pending = _pendingFees(listing);
if (pending != 0) {
    feeCredit[msg.sender] += pending;
    emit EarningsAccrued(msg.sender, listingId, pending);
}

uint256 oldWeight = listing.weight;
uint256 slot = listing.slot;
if (newWeight > oldWeight) {
    _addTreeWeight(slot, newWeight - oldWeight);
} else if (newWeight < oldWeight) {
    _removeTreeWeight(slot, oldWeight - newWeight);
}
totalWeight = totalWeight - oldWeight + newWeight;
weightedBackingTotal = weightedBackingTotal - oldWeight * oldBacking + newWeight * newBacking;

Function: claimTopSpot(uint256 listingId)

Seizes the top-backed-listing slot for one of the caller's active listings. If the slot is vacant it is taken with no threshold; otherwise the listing's backing must clear the incumbent by topThresholdBps, currently 10%.

ATTRIBUTE VALUE
Selector 0x0986a5a1
Parameters listingId
Access Depositor only, nonReentrant
FLAG OBSERVATION
☑ An early return when the caller already holds the top is load-bearing: without it the vacate path would settle and zero the holder's own in-progress pot, then re-top them, wiping their accrued share.
☑ The threshold comparison is cross-multiplied (value × BPS >= topValue × (BPS + threshold)) so no intermediate division rounds the bar down in the challenger's favour.
◇ Effects only — it credits feeCredit and transfers no ETH — but is still marked nonReentrant for consistency.
◇ Because richly-backed listings carry low selection weight, the top holder is drawn rarely and therefore holds the slot for a long time, compounding the incentive.
CONDITION REVERT
Listing not active ListingNotActive()
Caller is not the depositor NotDepositor()
Backing does not clear the bar TopNotBeaten()
function claimTopSpot(uint256 listingId) external nonReentrant {
    Listing storage listing = _activeOwned(listingId);

    uint256 currentId = topListingId;

    if (currentId == listingId) return;

    if (currentId != 0) {
        if (!_beatsTop(listing.value, currentId)) revert TopNotBeaten();
        _vacateTop();
    }

    _setTop(listingId);
}

function _beatsTop(uint256 value, uint256 currentId) internal view returns (bool) {
    return value * BPS >= listings[currentId].value * (BPS + topThresholdBps);
}

Keeper Functions

Function: processAcquisitions(uint256 maxCount)

Advances the canonical settlement queue. The caller chooses only how many to process; which requests are handled and in what order is fixed by nextSequenceToProcess. This is the function that actually allocates listings.

ATTRIBUTE VALUE
Selector 0xea502b3f
Parameters maxCount — gas batching bound
Access Permissionless, nonReentrant
Returns processed — number of sequences advanced
FLAG OBSERVATION
☑ Strictly ordered. A Pending request at the head that has not yet passed its deadline stops the loop, so nothing settles out of turn.
☑ Every terminal path — fulfilled, expired, timed out — first activates the sequence's reserved staged batch, so a timeout changes only the missing selection, never the staged prefix.
☑ Re-checks live price against the tolerances snapshotted at request time, converting excessive drift into a pull refund and leaving the would-be selected listing active.
☑ An empty pool at settlement time refunds the purchaser with no external call.
◇ Also invoked opportunistically from the VRF callback via a gas-capped self-call, so in practice most acquisitions settle inside the Chainlink callback rather than needing a separate keeper.
△ If the head request is Pending and its coordinator callback never arrives, the queue stalls until block.number passes wordDeadlineBlock — 30 blocks, roughly six minutes.
STEP ACTION
1 Read the request at nextSequenceToProcess; revert if the sequence mapping is inconsistent
2 Pending and before deadline: break out of the loop
3 Pending past deadline, or TimedOut: activate the reserved batch, credit the escrowed fee as a pull refund
4 Ready: activate the reserved batch, then run selection and fee distribution
5 Notify the rewards module of settlement or refund, decrement counters, advance the sequence
CONDITION REVERT
Sequence mapping inconsistent SequenceInvariantBroken()
Status is None, Fulfilled, Expired or Refunded at the head SequenceInvariantBroken()
function processAcquisitions(uint256 maxCount) external nonReentrant returns (uint256 processed) {
    while (processed < maxCount && nextSequenceToProcess <= lastIssuedSequence) {
        uint64 sequence = nextSequenceToProcess;
        uint256 requestId = requestIdAtSequence[sequence];
        Acquisition storage acquisition = acquisitions[requestId];
        AcquisitionMeta storage meta = acquisitionMeta[requestId];
        if (requestId == 0 || meta.sequence != sequence) revert SequenceInvariantBroken();

        AcquisitionStatus status = acquisition.status;
        if (status == AcquisitionStatus.Pending) {
            if (block.number <= meta.wordDeadlineBlock) break;
            pendingAcquisitionCount -= 1;
            _activateReservedBatch(sequence);
            uint256 refund = _creditAcquisitionRefund(acquisition, AcquisitionStatus.Expired);
            emit AcquisitionExpired(requestId, sequence, acquisition.purchaser, refund);
        } else if (status == AcquisitionStatus.TimedOut) {
            _activateReservedBatch(sequence);
            uint256 refund = _creditAcquisitionRefund(acquisition, AcquisitionStatus.Expired);
            emit AcquisitionExpired(requestId, sequence, acquisition.purchaser, refund);
        } else if (status == AcquisitionStatus.Ready) {
            _activateReservedBatch(sequence);
            _settleReadyAcquisition(requestId, acquisition, meta);
        } else {
            revert SequenceInvariantBroken();
        }

        _finishAcquisition(requestId, acquisition, meta, msg.sender);
        unchecked {
            ++nextSequenceToProcess;
            ++processed;
        }
    }
}

Function: rawFulfillRandomWords(uint256 requestId, uint256[] randomWords)

Chainlink's Verifiable Random Function (VRF) fulfillment entrypoint. The coordinator is the only authorised caller. The contract hand-rolls this rather than inheriting VRFConsumerBaseV2Plus, because that base declares a non-virtual callback and pulls in a ConfirmedOwner that would clash with Solady's Ownable.

ATTRIBUTE VALUE
Selector 0x1fe543e3
Parameters requestId; randomWords
Access VRF coordinator only
FLAG OBSERVATION
☑ Unknown, duplicate, terminal and malformed fulfilments return without reverting, so bad input from the coordinator does not appear able to permanently brick a callback.
☑ Callback liability is released exactly once per request via callbackObserved, before any classification, because Chainlink bills for an attempted callback regardless of outcome.
☑ A word arriving after wordDeadlineBlock is permanently classified TimedOut rather than being used, closing the late-callback steering window.
☑ The fast-path settlement self-call is made with a fixed gas stipend and its return data is deliberately not copied, so a reverting callee cannot consume the parent's return reserve with an oversized revert payload.
△ The randomness is only as good as the coordinator address in storage, and that address is owner-replaceable while the pool is quiescent.
STEP ACTION
1 Reject any caller other than s_vrfCoordinator
2 Release the callback liability once for a request this contract issued
3 Return early if the acquisition is not Pending
4 If past the deadline, mark TimedOut and return
5 If the word array is empty, return
6 Cache the word, mark Ready, and attempt the gas-capped fast-path settlement
CONDITION REVERT
Caller is not the coordinator OnlyCoordinator()
function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external {
    if (msg.sender != address(s_vrfCoordinator)) revert OnlyCoordinator();
    fulfillRandomWords(requestId, randomWords);
}

function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal {
    Acquisition storage acquisition = acquisitions[requestId];

    if (acquisition.status != AcquisitionStatus.None && !callbackObserved[requestId]) {
        callbackObserved[requestId] = true;
        uint256 count = unfulfilledVrfCount;
        if (count != 0) unfulfilledVrfCount = count - 1;
    }

    if (acquisition.status != AcquisitionStatus.Pending) {
        emit FulfillmentIgnored(requestId, acquisition.status);
        return;
    }

    AcquisitionMeta storage meta = acquisitionMeta[requestId];
    if (block.number > meta.wordDeadlineBlock) {
        acquisition.status = AcquisitionStatus.TimedOut;
        pendingAcquisitionCount -= 1;
        emit RandomnessTimedOut(requestId, meta.sequence, meta.wordDeadlineBlock, block.number);
        return;
    }

    if (randomWords.length == 0) {
        emit FulfillmentIgnored(requestId, acquisition.status);
        return;
    }

    meta.randomWord = randomWords[0];
    acquisition.status = AcquisitionStatus.Ready;
    pendingAcquisitionCount -= 1;
    emit RandomnessCached(requestId, meta.sequence, randomWords[0]);

    _tryProcessCallbackHead(meta.sequence);
}

Function: payoutFees()

Pushes accrued protocol fees out. A protocolFeeToTokenBps slice goes to the FWAToken buyback reserve and the remainder to the payout address. Permissionless.

ATTRIBUTE VALUE
Selector 0x4c5b2beb
Access Permissionless, nonReentrant
Returns amount — the slice sent to the payout address
FLAG OBSERVATION
◇ protocolFeeToTokenBps is 10000 at snapshot, so the entire accrued balance goes to FWAToken and the payout address receives zero.
☑ Zeroes accruedOwnerFees before any transfer.
◇ Permissionless, so anyone can force the sweep; there is no owner-only gate on when fees leave.
CONDITION REVERT
Nothing accrued NoOwnerFees()
function payoutFees() external nonReentrant returns (uint256 amount) {
    uint256 accrued = accruedOwnerFees;
    if (accrued == 0) revert NoOwnerFees();
    accruedOwnerFees = 0;

    uint256 tokenPortion = token == address(0) ? 0 : accrued * protocolFeeToTokenBps / BPS;
    amount = accrued - tokenPortion;

    address to = payoutAddress;
    if (amount != 0) SafeTransferLib.forceSafeTransferETH(to, amount);
    if (tokenPortion != 0) {
        SafeTransferLib.forceSafeTransferETH(token, tokenPortion);
        emit ProtocolFeesToToken(tokenPortion);
    }
    emit FeesPaidOut(to, amount);
}

Function: finalizeUnsettled(uint256 listingId)

After finalizeWindow (7 days) lapses with neither party resolving, anyone may finalize the default outcome: the NFT goes to the purchaser and the backing returns to the depositor less the protocol cut.

ATTRIBUTE VALUE
Selector 0x9eb60921
Access Permissionless, nonReentrant
FLAG OBSERVATION
☑ Guarantees neither asset locks permanently if both parties go silent.
☑ NFT delivery is best-effort here, so a hostile collection cannot prevent the depositor's ETH from returning.
◇ The default favours the purchaser on the NFT and the depositor on the ETH — the same economics as keepNFT.
CONDITION REVERT
Listing not allocated ListingNotAllocated()
Less than 7 days since allocation SettlementWindowNotElapsed()
function finalizeUnsettled(uint256 listingId) external nonReentrant {
    Listing storage listing = listings[listingId];

    if (listing.status != ListingStatus.Allocated) revert ListingNotAllocated();
    if (block.timestamp < uint256(listing.allocatedAt) + finalizeWindow) revert SettlementWindowNotElapsed();

    (address depositor, address purchaser,) = _settleNFTToPurchaser(listingId, listing, false);
    emit UnsettledFinalized(listingId, purchaser, depositor);
}

Admin Functions

Function: setUint(uint256 key, uint256 value)

The owner's primary economic control. A single dispatcher covering 17 numeric parameters keyed by FWAConfigKeys constants. The former per-knob setters were merged into three generic dispatchers to fit the runtime under EIP-170. The three together accept 25 of the 28 FWAConfigKeys constants.

ATTRIBUTE VALUE
Selector 0x61e3c944
Parameters key — a FWAConfigKeys constant; value
Access onlyOwner
FLAG OBSERVATION
☑ Reverts AcquisitionStateLocked() whenever any acquisition is unresolved, so the owner structurally cannot re-price a draw that is already in flight.
☒ No timelock. Once the pool is idle, every change below takes effect in the same block it is submitted.
☒ OWNER_ACQUISITION_FEE_BPS is bounded only by BPS, permitting 100%. The owner can redirect the entire depositor fee stream to the protocol at any idle moment.
☑ OWNER_SETTLEMENT_FEE_BPS is bounded by MAX_OWNER_SETTLEMENT_FEE_BPS = 500, so the settlement cut on this path cannot exceed 5% of backing.
☑ SETTLEMENT_DISCOUNT_BPS is bounded to the range 8000–9500 and must not fall below ownerSettlementFeeBps, keeping the purchaser's payout non-negative.
△ SURCHARGE_BPS is deliberately uncapped.
△ MIN_BACKING has no bound, so it could be set high enough to block all new deposits.
☑ Window parameters enforce their cross-invariant: SETTLEMENT_WINDOW <= FINALIZE_WINDOW in both directions.
☑ SELECTION_TIMEOUT_BLOCKS must exceed requestConfirmations + 2 and cap at 7,200, so the callback window can never be set below the confirmation depth.
KEY BOUND VALUE AT SNAPSHOT
VRF_SUB_ID Non-zero; requires no unfulfilled callbacks 0xbe3fb16e...0ffd33
REQUEST_CONFIRMATIONS 3–200, and selectionTimeoutBlocks >= value + 2 3
MAX_ACTIVATIONS_PER_ACQUISITION 1–16 6
SELECTION_TIMEOUT_BLOCKS requestConfirmations + 2 to 7,200 30
MAX_ACQUISITIONS_PER_TX Non-zero 5
SURCHARGE_BPS Uncapped 250
SELECTION_SLIPPAGE_BPS ≤ 10000 1000
TOP_LISTING_SHARE_BPS ≤ 10000 100
TOP_THRESHOLD_BPS ≤ 10000 1000
SETTLEMENT_DISCOUNT_BPS 8000–9500, >= ownerSettlementFeeBps 9000
OWNER_ACQUISITION_FEE_BPS ≤ 10000 100
OWNER_SETTLEMENT_FEE_BPS ≤ 500, <= settlementDiscountBps 100
SETTLEMENT_WINDOW <= finalizeWindow 86,400
FINALIZE_WINDOW >= settlementWindow 604,800
MIN_BACKING Unbounded 0.05 ETH
PROTOCOL_FEE_TO_TOKEN_BPS ≤ 10000 10000
MAX_STAGED_LISTINGS Unbounded, 0 = unlimited 0
CONDITION REVERT
Caller is not the owner Unauthorized()
An acquisition is unresolved AcquisitionStateLocked()
Unknown key or out-of-bounds value InvalidConfig()
function setUint(uint256 key, uint256 value) external onlyOwner {
    if (unsettledAcquisitionCount != 0) revert AcquisitionStateLocked();
    if (key == FWAConfigKeys.VRF_SUB_ID) {
        if (value == 0) revert InvalidConfig();
        if (unfulfilledVrfCount != 0) revert AcquisitionStateLocked();
        vrfSubId = value;
    } else if (key == FWAConfigKeys.SETTLEMENT_DISCOUNT_BPS) {
        if (value < MIN_SETTLEMENT_DISCOUNT_BPS || value > MAX_SETTLEMENT_DISCOUNT_BPS) {
            revert InvalidConfig();
        }
        if (value < ownerSettlementFeeBps) revert InvalidConfig();
        settlementDiscountBps = value;
    } else if (key == FWAConfigKeys.OWNER_ACQUISITION_FEE_BPS) {
        ownerAcquisitionFeeBps = _bpsCapped(value);
    } else if (key == FWAConfigKeys.OWNER_SETTLEMENT_FEE_BPS) {
        if (value > MAX_OWNER_SETTLEMENT_FEE_BPS || value > settlementDiscountBps) revert InvalidConfig();
        ownerSettlementFeeBps = value;
    }
    // … 13 further keys elided; branches reordered here for readability — see the verified source
    else {
        revert InvalidConfig();
    }
    emit ConfigSet(key, value);
}

Function: setAddr(uint256 key, address value)

Sets the three address-valued parameters: the whitelist manager, the VRF coordinator, and the payout address.

ATTRIBUTE VALUE
Selector 0xeba36dbd
Access onlyOwner
FLAG OBSERVATION
☒ VRF_COORDINATOR is replaceable. A coordinator that returns chosen words would let the replacer control every selection. The only gate is that the pool must be quiescent — there is no delay and no second signature.
☒ PAYOUT_ADDRESS can be redirected to any address at any time, with no state gate at all.
☑ Zero address is rejected for every key except WHITELIST_MANAGER, where zero revokes the manager.
◇ The source notes a new coordinator's subscription must already list this contract as a consumer, but the contract does not verify that.
CONDITION REVERT
Caller is not the owner Unauthorized()
Zero address on a key other than WHITELIST_MANAGER InvalidConfig()
Coordinator change while acquisitions are in flight AcquisitionStateLocked()
Unknown key InvalidConfig()
function setAddr(uint256 key, address value) external onlyOwner {
    if (key == FWAConfigKeys.WHITELIST_MANAGER) {
        whitelistManager = value;
    } else if (value == address(0)) {
        revert InvalidConfig();
    } else if (key == FWAConfigKeys.VRF_COORDINATOR) {
        if (unsettledAcquisitionCount != 0 || unfulfilledVrfCount != 0) revert AcquisitionStateLocked();
        s_vrfCoordinator = IVRFCoordinatorV2Plus(value);
    } else if (key == FWAConfigKeys.PAYOUT_ADDRESS) {
        payoutAddress = value;
    } else {
        revert InvalidConfig();
    }
    emit ConfigSet(key, uint256(uint160(value)));
}

Function: setBool(uint256 key, bool value)

Toggles five switches: whether retained settlement penalties go to the protocol, whether acquisitions are open, emergency withdraw-only mode, whitelist enforcement, and whether purchasers may take their settlement as FWAToken.

ATTRIBUTE VALUE
Selector 0x97cedc76
Access onlyOwner
FLAG OBSERVATION
△ ACQUISITIONS_ENABLED is the effective pause switch. Disabling it stops new acquisitions but leaves every existing listing withdrawable, so it is a soft pause rather than a freeze.
☑ WITHDRAW_ONLY is the emergency mode: it blocks new deposits, backing increases and acquisitions, while leaving withdrawals and decreases open. On the paths we read it does not block depositor exits.
☒ Unlike setUint, setBool has no unsettledAcquisitionCount gate. WHITELIST_ENABLED and RETAINED_TO_PROTOCOL can therefore be flipped while acquisitions are in flight, and a retainedToProtocol flip changes the destination of the retained penalty on listings already allocated.
◇ Enabling acquisitions also starts the rewards module's emission clock, idempotently.
CONDITION REVERT
Caller is not the owner Unauthorized()
Unknown key InvalidConfig()
function setBool(uint256 key, bool value) external onlyOwner {
    if (key == FWAConfigKeys.RETAINED_TO_PROTOCOL) {
        retainedToProtocol = value;
    } else if (key == FWAConfigKeys.ACQUISITIONS_ENABLED) {
        acquisitionsEnabled = value;
        IFWARewards r = rewards;
        if (value && address(r) != address(0)) r.startEmission();
    } else if (key == FWAConfigKeys.WITHDRAW_ONLY) {
        withdrawOnly = value;
    } else if (key == FWAConfigKeys.WHITELIST_ENABLED) {
        whitelistEnabled = value;
    } else if (key == FWAConfigKeys.ACCEPT_BID_AS_TOKENS_ENABLED) {
        acceptBidAsTokensEnabled = value;
    } else {
        revert InvalidConfig();
    }
    emit ConfigSet(key, value ? 1 : 0);
}

Function: setRewards(address module)

One-time wiring of the external FWARewards module. Also derives and stores the token address by reading it back from the module.

ATTRIBUTE VALUE
Selector 0xec38a862
Access onlyOwner, callable once
FLAG OBSERVATION
☑ Irreversible. Once rewards is non-zero the function reverts, so the module cannot be swapped for a hostile one later.
☑ Requires an empty pool (activeListingCount == 0, no staged or reserved listings), so it can only be called during the loading phase before any depositor is exposed.
☑ Verifies the module points back at this contract (r.fwa() == address(this)) and exposes a non-zero token.
◇ token is never settable directly — it is whatever the module reported at wiring time.
CONDITION REVERT
Caller is not the owner Unauthorized()
Zero address, or rewards already set InvalidConfig()
Pool is not empty InvalidConfig()
Module's token is zero or its fwa() mismatches InvalidConfig()
An acquisition is unresolved AcquisitionStateLocked()
function setRewards(address module) external onlyOwner {
    if (module == address(0) || address(rewards) != address(0)) revert InvalidConfig();
    if (unsettledAcquisitionCount != 0) revert AcquisitionStateLocked();
    if (activeListingCount != 0 || stagedCount != 0 || reservedStagedCount != 0) revert InvalidConfig();
    IFWARewards r = IFWARewards(module);
    address rewardToken = r.token();
    if (rewardToken == address(0) || r.fwa() != address(this)) revert InvalidConfig();
    rewards = r;
    token = rewardToken;
    if (acquisitionsEnabled) r.startEmission();
    emit RewardsConfigured(module, rewardToken);
}

View Functions

Function: acquisitionFee()

The expected-value price of one acquisition, before the VRF service fee. This is the pricing core of the contract.

ATTRIBUTE VALUE
Selector 0x38f5f005
Access Public view
Returns EV × (1 + surchargeBps/BPS) in wei
FLAG OBSERVATION
☑ Because weight = 1e36 / value, the product weight × value is approximately 1e36 for every listing, so weightedBackingTotal / totalWeight reduces to the Harmonic Mean of all active backings. We verified this independently: computing the harmonic mean of all 5,770 backings at block 25733839 gives 0.079742869391249362 ETH, matching the contract exactly.
◇ The harmonic mean (0.0797 ETH) sits far below the arithmetic mean (0.2160 ETH). That gap is the inverse weighting: a purchaser is overwhelmingly likely to draw a cheap listing, and the price says so.
△ This prices the expected backing, not the expected market value of the NFT. Nothing on-chain relates the two.
◇ Returns 0 for an empty pool rather than reverting.
function acquisitionFee() public view returns (uint256) {
    if (totalWeight == 0) return 0;
    uint256 ev = weightedBackingTotal / totalWeight;
    return ev * (BPS + surchargeBps) / BPS;
}

Function: quoteAcquisitionPrice()

Returns the full native cost of one acquisition as a triple.

ATTRIBUTE VALUE
Selector 0x987df4cd
Access External view
Returns fee (pool), vrf (service), total
FLAG OBSERVATION
△ vrf derives from tx.gasprice inside the service, so an eth_call made without the intended gas price returns a figure well below what the transaction actually pays. Reading it at zero gas price returns zero.
◇ At block 25733839 the pool fee is 0.081736 ETH. The VRF component is only observable from what purchasers actually paid: across the 76 VrfServiceFeePaid events in the preceding 600 blocks the median was 0.000273 ETH, about a third of a percent of the trade.
function quoteAcquisitionPrice() external view returns (uint256 fee, uint256 vrf, uint256 total) {
    fee = acquisitionFee();
    vrf = vrfServiceFee();
    total = fee + vrf;
}

Function: pendingFees(uint256 listingId)

The acquisition-fee share an active listing has accrued but not yet had credited.

ATTRIBUTE VALUE
Selector 0xa2b93478
Access External view
FLAG OBSERVATION
☑ The subtraction saturates at zero rather than underflowing, because a floored accumulator reading can sit just below a ceiled feeDebt when nothing has accrued.
◇ Rounding on the paths we read biases toward the contract, consistent with the non-negative aggregate margin observed at the snapshot. Summed across all 5,770 active listings this figure was 70.370386501 ETH.
◇ Returns 0 for any listing that is not Active; a removed listing's fees are already in its depositor's feeCredit.
function pendingFees(uint256 listingId) external view returns (uint256) {
    Listing storage listing = listings[listingId];
    if (listing.status != ListingStatus.Active) return 0;
    return _pendingFees(listing);
}

function _pendingFees(Listing storage listing) internal view returns (uint256) {
    uint256 acc = listing.feeShare * accFeePerEV / SCALE;
    uint256 debt = listing.feeDebt;
    return acc > debt ? acc - debt : 0;
}

Function: treeRootWeight()

The Segment Tree root, which should always equal totalWeight.

ATTRIBUTE VALUE
Selector 0x1b9bc525
Access External view
FLAG OBSERVATION
☑ Exposed specifically so the tree's internal consistency can be checked from outside. At block 25733839 both read 72,357,566,815,035,813,943,552 after 149,854 insertions and 144,084 removals.
◇ A divergence between this and totalWeight would indicate tree corruption and would make selection probabilities wrong.
function treeRootWeight() external view returns (uint256) {
    return tree[1];
}

Function: canRescueRewards()

A migration guard the rewards module consults before permitting an owner token rescue.

ATTRIBUTE VALUE
Selector 0xd3c4a761
Access External view
Returns withdrawOnly && unsettledAcquisitionCount == 0
FLAG OBSERVATION
◇ Rewards can only be rescued once the pool has been put into emergency withdraw-only mode and fully quiesced. Both conditions are owner-reachable, so this is a sequencing constraint rather than a limit on the owner's authority.
function canRescueRewards() external view returns (bool) {
    return withdrawOnly && unsettledAcquisitionCount == 0;
}