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.
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.
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.