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.
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.
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.
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
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.
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.
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
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
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.
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.
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.
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
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
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.
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.
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()
functionsetUint(uint256key,uint256value)externalonlyOwner{if(unsettledAcquisitionCount!=0)revertAcquisitionStateLocked();if(key==FWAConfigKeys.VRF_SUB_ID){if(value==0)revertInvalidConfig();if(unfulfilledVrfCount!=0)revertAcquisitionStateLocked();vrfSubId=value;}elseif(key==FWAConfigKeys.SETTLEMENT_DISCOUNT_BPS){if(value<MIN_SETTLEMENT_DISCOUNT_BPS||value>MAX_SETTLEMENT_DISCOUNT_BPS){revertInvalidConfig();}if(value<ownerSettlementFeeBps)revertInvalidConfig();settlementDiscountBps=value;}elseif(key==FWAConfigKeys.OWNER_ACQUISITION_FEE_BPS){ownerAcquisitionFeeBps=_bpsCapped(value);}elseif(key==FWAConfigKeys.OWNER_SETTLEMENT_FEE_BPS){if(value>MAX_OWNER_SETTLEMENT_FEE_BPS||value>settlementDiscountBps)revertInvalidConfig();ownerSettlementFeeBps=value;}// … 13 further keys elided; branches reordered here for readability — see the verified sourceelse{revertInvalidConfig();}emitConfigSet(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
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 nounsettledAcquisitionCount 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.
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.
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.
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.
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.
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.
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.