Truth Auction

When Zoltar forks, Placeholder evaluates each resulting child universe after migration. If a child pool lacks the ETH needed to fully collateralize its open interest, Placeholder initiates a truth auction for that pool. The fork may have been triggered either by a direct Zoltar fork or by the Placeholder Escalation Game.

During the migration period, security vault holders choose which child universe to support by migrating their positions into the corresponding child security vault. This preserves disagreement by splitting the parent market into separate branches. However, ETH is not duplicated across those branches. Each child pool receives only the collateral, migrated REP state, and converted child-universe REP backing that actually move into it.

Because collateral follows migration rather than being copied in full, a child pool may inherit the correct share balances, vault state, and REP-backed obligations while still lacking enough ETH to reopen cleanly. When that happens, the protocol sells child-universe REP for ETH through a truth auction, using the proceeds to restore the collateral shortfall and make open-interest holders whole. The auction repairs the pool's funding gap; it does not determine which branch is true.

The design is dual-capped because the auction has two limits at once: it is trying to raise a fixed amount of ETH, and it is not allowed to sell more than a fixed amount of child-universe REP. Strong demand can hit the ETH target first, or it can bind the REP sale cap first. When finalization finds a non-empty winning prefix, weak demand does not leave unsold REP behind. Instead, the auction fixes that winning bid prefix, refunds lower bids, and still clears the full REP cap at one synthetic uniform price. Placeholder runs this process through the UniformPriceDualCapBatchAuction contract, then writes winning claims back into vault state rather than paying out simple token transfers, so bidders buy into the repaired child pool and its matching security-bond allowance instead of merely receiving REP.

Operational Lifecycle

For any child pool with a collateral shortfall, the truth auction can only begin after fork migration has closed. The migration window lasts SecurityPoolUtils.MIGRATION_TIME - 8 weeks from the parent universe fork time. Until that window ends, vault holders can still choose which child universe to support, so the protocol cannot know the final collateral shortfall.

After migration closes, anyone may call startTruthAuction. The forker then starts the underlying auction for the affected child pool, using the pool's ETH repair target and REP sale cap. Bidders submit ETH directly to the auction during the one-week bidding window.

When bidding ends, anyone may call finalizeTruthAuction. Finalization clears the batch auction, sends the raised ETH into the repair path, and computes the accounting rates used to settle bidders.

After finalization, winning bids are written into child-pool vault accounting. Winners receive ownership in the repaired child pool plus their proportional share of the auctioned security-bond allowance, rather than a direct REP token transfer.

Shortfall child pool lacks ETH Start Auction repair target set Bid ETH supplied Settle vault accounting repaired

Auction Lifecycle After migration closes, a child-pool shortfall can be repaired through auction. ETH bids restore collateral, while winning claims are settled into vault accounting. REP doesn't leave the system.

Phase External call Auction call Who can call it Effect
Start auction startTruthAuction startAuction Anyone may call the wrapper; the forker calls the owner-only auction function. Opens the auction using the child pool's repair target and REP sale cap.
Submit bid Bidder calls the auction directly submitBid Any bidder during the one-week auction window, with msg.value >= minBidSize. Supplies ETH demand for the repair. The minimum bid is max(ethRaiseCap / 100000, 1 wei).
Finalize auction finalizeTruthAuction finalize Anyone may call the wrapper after the auction window has passed; the forker calls the owner-only auction function. Clears the auction, routes raised ETH into the repair path, and computes bidder settlement rates.
Refund losing bids settleAuctionBids refundLosingBidsFor Anyone may fetch a bounded bid page with getBidderBidPage or getBidPageAtTick, then filter those results down to one bidder or vault and pass the selected (tick, bidIndex) references into the forker's refund batch. Allows currently losing bids to exit before the auction is finalized.
Settle winning bids claimAuctionProceeds or settleAuctionBids withdrawBids Anyone may fetch bounded bid pages, group the returned bids for one bidder or vault, then settle the selected (tick, bidIndex) entries in one or more bounded batches instead of settling the entire bid set at once. Writes winning claims into child-pool vault accounting as pool ownership plus proportional security-bond allowance.

The direct auction finalize path allows finalization at auctionStarted + AUCTION_TIME. The public finalizeTruthAuction wrapper is slightly stricter and requires the timestamp to be greater than truthAuctionStarted + SecurityPoolUtils.AUCTION_TIME.

How Clearing Works

Bidders submit ETH at discrete price ticks, where each tick encodes an ETH-per-REP limit price. The auction then walks demand from the highest price downward. A bid above the clearing tick wins in full, a bid below it loses in full, and bids at the clearing tick share the last fill. In the uniform-clearing branch that last fill is First in First Out within the tick, so earlier same-tick bids are consumed before later ones.

The tick math follows the contract's tickToPrice function. Tick 0 means 1 ETH/REP. Each step up multiplies price by roughly 1.0001, and each step down divides by the same factor, so the price at tick tickIndex is approximately 1.0001tickIndex ETH/REP. In other words, a higher tick means the bidder is willing to pay more ETH per REP, which is why higher ticks are evaluated first during clearing.

Two caps define the uniform-clearing stopping point. The first cap is ethRaiseCap, the amount of ETH the child pool is trying to restore. The second cap is maxRepBeingSold, the maximum child-universe REP inventory the pool is willing to sell. As soon as cumulative demand is enough to hit either cap at some price, that price becomes the clearing tick and all winning bids settle from it at one price. That is a valid uniform clearing even when the REP cap binds before the ETH repair target is fully met. The contract finds that result by descending aggregate subtree totals, not by looping over every bid one by one.

When demand never reaches a full clearing price, the auction switches into the underfunded branch. Finalization then walks demand from the highest tick downward, fixes the largest winning prefix whose synthetic execution price does not exceed the lowest winning tick's limit, stores that lowest winning tick as clearingTick, refunds lower ticks, and, when that winning prefix is non-empty, sells the full REP cap at underfundedThreshold. If no winning prefix exists, every bid refunds instead. Winners still settle at one synthetic uniform price; the difference is that the winner set is the finalized prefix boundary rather than a threshold-only price test.

The interactive example below is where those branches become concrete. The Binding condition output reports one of four outcomes: ETH cap, REP cap, both caps, or underfunded. The first three are uniform-clearing outcomes, while underfunded is the weak-demand path.

Under weak demand, the auction is designed to maximize retained ETH while still clearing the full REP cap. In the underfunded branch, the contract does not sell only the fraction of REP matched by submitted ETH. It finds the largest high-to-low winning prefix whose members can all accept one synthetic all-REP price, then clears the full REP cap against that prefix. This branch only applies while normal funded clearing still fails. If adding a lower positive bid would make the REP cap bind at that lower tick, the contract does not stay underfunded with a narrower winner set. It switches into the normal funded-clearing branch at that lower tick instead.

Bid position Uniform-clearing branch Underfunded branch
Above the binding level Wins in full at the uniform clearing price. Wins only if its tick is at or above the finalized clearingTick winner boundary.
At the binding level Shares the marginal fill FIFO inside that tick. Shares the full REP cap pro rata by winning ETH at the synthetic underfunded clearing price.
Below the binding level Loses and is refunded. Loses and is refunded.

Worked Underfunded Examples

The examples below use the intended weak-demand rule: sell all REP to the largest feasible high-to-low winning prefix, then settle that prefix pro rata by winning ETH. In all three examples, maxRepBeingSold = 100 REP and ethRaiseCap = 1,000 ETH, so the ETH cap never binds and the auction stays underfunded only if cumulative demand remains below the REP cap at every active tick.

Example Bid ladder Feasible winning prefix Outcome
Two-bidder underfunded prefix Bob: 6 ETH @ 4 ETH/REP
Alice: 4 ETH @ 2 ETH/REP
At Bob's higher tick, cumulative demand is 6 / 4 = 1.5 REP, so the REP cap is not reached.
At Alice's lower tick, cumulative demand is 10 / 2 = 5 REP, so funded clearing still fails.
The final synthetic all-REP price is 10 / 100 = 0.10 ETH/REP, which stays below both winning limits.
The system keeps 10 ETH.
Bob and Alice share all 100 REP pro rata by winning ETH.
Bob receives 60 REP and Alice receives 40 REP.
Three-bidder underfunded prefix Alice: 5 ETH @ 5 ETH/REP
Bob: 3 ETH @ 3 ETH/REP
Carol: 2 ETH @ 2 ETH/REP
At the active ticks, cumulative demand is 5 / 5 = 1 REP, then 8 / 3 ≈ 2.67 REP, then 10 / 2 = 5 REP.
All of those remain below the 100 REP cap, so the funded branch never triggers.
The final synthetic all-REP price is again 10 / 100 = 0.10 ETH/REP, which all three bids accept.
The system keeps 10 ETH.
Alice, Bob, and Carol share all 100 REP pro rata by winning ETH.
Alice receives 50 REP, Bob 30 REP, and Carol 20 REP.
Tiny-demand edge case Alice: 1 ETH @ 0.10 ETH/REP
No other positive-price bids.
At Alice's tick, cumulative demand is 1 / 0.10 = 10 REP, so funded clearing still fails.
Alice alone is feasible because 1 / 100 = 0.01 ETH/REP, which stays below her 0.10 limit.
The system keeps 1 ETH.
Alice receives all 100 REP.
That extreme result is still intended: once the auction is underfunded, the rule is to clear the full REP cap at the best feasible all-REP price, not to leave unsold REP behind.

Boundary note: if a lower positive bid would make cumulative demand reach the REP cap at that lower tick, the auction no longer stays in the underfunded branch. It becomes a normal funded clearing at that tick, with the marginal lower-price tick filled only up to the REP cap. If that tick contributes more ETH than the cap accepts, the unfilled remainder is refunded.

Gas and Spam Bounds

The main spam risk is a bidder trying to fill bid slots or tick levels cheaply enough to block later demand. The auction avoids that failure mode by not capping total bids or active price levels at all.

Finalization remains bounded without those caps because it does not iterate over raw bids. Same-tick bids append to per-tick arrays for later batched withdrawal. Read helpers expose bounded offset/limit slices of bids at one tick or for one bidder, and settlement later uses selected TickIndex references from those reads to process bids in bounded batches. Clearing itself uses aggregate ETH totals stored in an AVL tree keyed by tick. The tree exists so the contract can keep all valid price levels open during bidding, yet still find the highest winning region by descending aggregate subtree totals instead of scanning every submitted bid. Finalization therefore pays for tree descent across the finite tick domain rather than for the total number of bids in the auction. The valid tick range is finite: [-524288, 524288], or 1,048,577 possible price levels. An AVL tree containing every possible tick has maximum height 28, so finalize() descends bounded tree paths and pruned subtrees instead of risking an out-of-gas loop over every submitted bid.

The gas tests cover both funded and underfunded finalization over a synthetic max-depth tree for the full tick domain. They assert each path remains below 20,000,000 gas and print measured gas values in the test output, so the documented worst case is a max-depth AVL-tree finalization path that still stays under that bound.

Settlement Modes

Currently losing bids can be refunded before finalization once current demand is enough to find a clearing tick. Only ticks below that found clearing tick can be withdrawn through the pre-finalization refund path; binding or potentially winning bids stay in the auction.

In the underfunded branch, finalization computes underfundedThreshold from the finalized underfundedWinningEth after the winning prefix is fixed. If underfundedWinningEth > 0, the stored clearingTick marks the lowest winning tick and underfunded withdrawals settle bids at ticks >= clearingTick. If no winning prefix exists, the contract leaves totalRepPurchased at zero, sets underfundedThreshold to type(uint256).max, and refunds every bid. The contract still records aggregate submitted ethRaised, but only underfundedWinningEth is retained after losing bids are refunded.

underfundedThreshold = { ceil ( underfundedWinningEth PRICE_PRECISION maxRepBeingSold ) if underfundedWinningEth > 0 type(uint256).max otherwise

Underfunded ThresholdPRICE_PRECISION = 1e18. This is the synthetic execution price for the finalized winning prefix, not the only winner predicate. Underfunded winners are the bids at ticks >= clearingTick when underfundedWinningEth > 0, and those winners share the full REP sale cap pro rata by winning ETH. Solidity carries division dust forward so paged withdrawals reconcile to the finalized aggregate result.

Finalized case Bid treatment Why it works this way
Losing bids Receive ETH refunds. They did not contribute to the accepted clearing demand.
Normal winning bids above clearing tick Convert all accepted ETH into purchased REP at the uniform clearing price. Higher bids should receive the same execution price as the marginal winner.
Normal marginal clearing tick May be partially filled and partially refunded; same-tick bids fill in First in First Out submission order. First in First Out gives deterministic marginal allocation without iterating over all same-tick bids during finalization.
Underfunded winning ticks Ticks at or above clearingTick share the full REP cap pro rata by winning ETH at the synthetic underfundedThreshold price. Weak demand still clears at one synthetic uniform price, but the winner set is the finalized prefix boundary rather than a pure threshold-price cutoff.

That settlement is why a winning bid buys more than a token transfer. The forker withdraws finalized bid results, converts purchased REP into pool ownership with auctionPoolOwnershipPerRep, and adds the matching slice of auctionedSecurityBondAllowance to the bidder's vault. In other words, the bidder is buying a share of the repaired child-pool vault state, including the debt-bearing allowance that still has to be backed.

Zero-effective-price ticks are rejected when bids are submitted, so underfunded settlement cannot assign REP to bids whose rounded ETH/REP price is zero.

Both finalized allocation paths carry division dust across paged withdrawals. Normal clearing carries clearingRemainder through the uniform-price ETH-to-REP division, and underfunded clearing uses that same remainder carry while dividing the full REP cap across winning ETH. Later withdrawals may receive remainder carried from earlier integer division so bid-level withdrawals reconcile to the aggregate finalized REP allocation.

Interactive Clearing Example

The slider example shows the uniform-clearing path, the underfunded path, and how the same bid ladder reacts when the ETH cap, REP cap, and bid sizes change.

Try a simple auction clearing run

The simplified calculator uses three fixed price levels to show the same two high-level outcomes as the contract: strong demand clears at a uniform price, while weak demand still settles the qualifying winner set at one synthetic uniform price. It uses real-number arithmetic for readability; the contract ceilings the synthetic underfunded threshold, floors REP allocations, and carries remainders during paged withdrawals.

Modeuniform clearing near 3 ETH/REP
Binding conditionboth caps
ETH retained12 ETH
Winning ETH keptnot underfunded
Thresholdnot underfunded
Alice REP1.00 REP
Bob REP1.33 REP
Carol REP1.67 REP
Refunds1.00 ETH