Early access is a phase before public buying, in which the presale module (Presale_v1) buys a market’s issuance token on the curve for each buyer, holds it, and releases it to the buyer after the phase closes. The module admits buyers in stages, an allowlist stage and then a public stage. It limits how many tokens each address and all addresses together can receive, and it takes a fee from each deposit. This page uses “early access” for the phase and “presale module” for the contract.

How a purchase works

A buyer calls buyPresale with a deposit of collateral. The presale module:
  1. computes the early-access fee on the deposit (see The fee)
  2. reverts if the fee is at or above the deposit
  3. buys on the curve with the rest of the deposit, the execution budget
  4. reverts if the tokens minted are below the buyer’s minimum amount out, or if they take the global or per-address total above its cap
  5. adjusts the fee to the collateral the curve consumed, sends the fee to its fee treasury, and refunds any unspent collateral to the buyer
  6. creates a purchase record for the buyer and keeps the minted tokens. The code calls the record a Position and numbers it with positionId
The curve mints the tokens at the time of purchase, so supply and the curve price move with each purchase, as for any buy (Markets and curves). The curve’s buy fee applies to the presale module’s curve buy as to any other buy. The buyer receives the tokens by calling claimAll after early access closes.

States

The presale module has four states. Each one sets which calls succeed. The module can also have an end timestamp. At and after the end timestamp, the module treats itself as Closed whatever its stored state: purchases and registration revert, claimAll succeeds, and getPresaleState returns Closed. An end timestamp of 0 means none is set.

Transitions

setPresaleState changes the state. The code accepts:
  • a move forward in the order NotOpen → Whitelist → Public → Closed, skipping a state if needed, except NotOpen → Closed
  • a reopen from Closed to Whitelist or Public
Every other change reverts, including a call that sets the current state again. A move to Whitelist reverts while the allowlist root is zero. A move to Whitelist or Public reverts at and after the end timestamp. Early access ends when the state is set to Closed or when the end timestamp is reached. A move to Public starts the multiplier’s decay if no decay start time is set. A move to Closed records the close time.

Allowlist

The allowlist is a Merkle tree of addresses. The presale module stores its root (setMerkleRoot). Each leaf is the hash of one address (keccak256(abi.encodePacked(address))). During Whitelist, an address registers itself by calling addToWhitelistWithProof with its Merkle proof. A valid proof registers the caller. A second registration by the same address reverts. No function removes a registration. During Whitelist, only registered addresses can buy. During Public, every address can buy and registration is not used. The root can be set only while the state is NotOpen. After early access opens, the root and the set of addresses it admits are fixed.

Caps

The presale module counts the tokens each purchase mints, per address and in total. A purchase reverts if it takes either total above its cap. For a looped purchase, the count is the purchase record’s total: the tokens locked in its loans plus its direct tokens. setCaps sets both caps together, has no bounds and can be called in any state. A lower cap does not change existing purchase records. A later purchase that exceeds it reverts.

The fee

The early-access fee is taken from the deposit. Its rate comes from a base commission schedule, multiplied by the fee multiplier and capped at 9,000 bps (90%). b=min⁡ ⁣(⌊ck⋅m10 000⌋, 9 000),fee=⌊D⋅b10 000⌋b = \min\!\left(\left\lfloor \frac{c_k \cdot m}{10\,000} \right\rfloor,\ 9\,000\right), \qquad \text{fee} = \left\lfloor D \cdot \frac{b}{10\,000} \right\rfloor where:
  • DD is the deposit, in collateral
  • kk is the loop count of the purchase: 0 for buyPresale, kk for buyPresaleWithLoops with kk rounds
  • ckc_k is the base commission for loop count kk, in bps (getBaseCommissionBps)
  • mm is the fee multiplier in bps, where 10,000 is 1× (getMultiplier). During Whitelist, mm is 10,000
  • bb is the rate charged, in bps
This fee is provisional. The execution budget is D−feeD - \text{fee}. When the purchase uses less than the execution budget, the presale module scales the fee down in proportion: feefinal=⌊fee⋅UD−fee⌋  if U<D−fee,refund=D−C−feefinal\text{fee}_{\text{final}} = \left\lfloor \text{fee} \cdot \frac{U}{D - \text{fee}} \right\rfloor \ \text{ if } U < D - \text{fee}, \qquad \text{refund} = D - C - \text{fee}_{\text{final}} where:
  • CC is the collateral that left the presale module during the purchase
  • UU is the amount used for the fee: CC for buyPresale; for buyPresaleWithLoops, the smaller of the execution budget and CC plus the net proceeds of the loans the purchase created
  • feefinal\text{fee}_{\text{final}} equals the provisional fee when UU equals the execution budget
The presale module sends the final fee to its own fee treasury. At launch that is a SplitterTreasury_v1 separate from the market fee treasury, created with the presale module by attachPresaleWithTreasury. Fees and routing describes how a splitter divides it. Example. The base commission for a direct purchase is 500 bps (5%), the multiplier is 1×, and a buyer deposits 1,000.00 collateral. The curve has room for only 760.00 collateral. When the curve consumes the whole execution budget, the final fee equals the provisional fee and the refund is 0.

The multiplier

The fee multiplier starts at an initial value and decreases to 1× over a decay duration. The decay starts at the first move to Public, unless the admin has set a start time with setStartTime. The multiplier falls fastest at the start of the decay. m(t)=10 000+⌊(m0−10 000)⋅(T−(t−t0)T)4⌋for t0≤t<t0+Tm(t) = 10\,000 + \left\lfloor (m_0 - 10\,000) \cdot \left( \frac{T - (t - t_0)}{T} \right)^{4} \right\rfloor \quad \text{for } t_0 \le t < t_0 + T where:
  • m0m_0 is the initial multiplier in bps (getInitialMultiplier)
  • TT is the decay duration in seconds (getDecayDuration)
  • t0t_0 is the decay start time (getStartTime)
  • tt is the block timestamp
Before t0t_0, or while no start time is set, mm is m0m_0. At and after t0+Tt_0 + T, mm is 10,000 (1×). During Whitelist the fee uses 1× whatever the multiplier’s value. Example. The initial multiplier is 30,000 (3×), the decay duration is 10 days, and the base commission for a direct purchase is 500 bps. A buyer deposits 1,000.00 collateral at different times after the move to Public, and the curve consumes the whole execution budget. With an initial multiplier of 10,000, the multiplier is 1× at every time and the rate equals the base commission.

Looped purchases

buyPresaleWithLoops(deposit, k, minAmountOut) buys with kk borrow-and-buy rounds through the market’s credit facility. Credit and looping explains a round. The presale module:
  1. computes the fee with base commission ckc_k
  2. calls the credit facility’s buyAndBorrow with the execution budget and kk rounds. The presale module is the borrower, so it holds every loan the rounds create
  3. buys on the curve with any collateral the credit facility returns
  4. creates a purchase record with one tranche per loan and the tokens from the final buy as direct tokens
The credit facility charges its origination fee on each loan as usual. The loop count must be at least 1 and below the length of the base commission schedule, and the credit facility also rejects a count above its own maximum. A looped purchase that mints no tokens reverts. Before it starts, a looped purchase requires two settings in the market’s authorizer: the presale module and this credit facility admitted as a pair (setPresaleCreditFacilityPair, admin only), and an ordinary permission for the presale module to call transferLoan on the credit facility.

Tranche unlocks

The base commission schedule has a matching list of price breakpoints, one fewer than the schedule’s length, in collateral per token. Tranche ii of any looped purchase record uses breakpoint ii. After early access closes, tranche ii becomes claimable when one of these holds:
  • the reference price is at or above breakpoint ii
  • the end timestamp has been reached
  • 730 days have passed since the first explicit close after the purchase record was created
Example. The schedule has three entries (direct, 1 round, 2 rounds) and the breakpoints are 1.10 and 1.25. A purchase record from a purchase with 2 rounds holds two loans. Early access has closed by setPresaleState, no end timestamp is set, and the reference price is 1.15.

Claiming

After early access ends, the owner of a purchase record calls claimAll(positionId). In one call the presale module:
  1. transfers the purchase record’s direct tokens to the owner, if they have not been claimed
  2. for each looped tranche that is claimable and not yet claimed:
    • transfers to the owner any issuance tokens the loan has released since the purchase (the issuance recorded at purchase less the loan’s current locked issuance)
    • transfers the loan to the owner with the credit facility’s transferLoan, if the loan is still active
After the transfer, the owner holds the loan. Tranches that are not yet claimable are skipped, and a later call claims them. Only the owner of a purchase record can call claimAll for it.

Claim permission

The market’s authorizer (AUT_Roles_v2) implements IPresaleClaimPermission_v1. While the presale module holds an unclaimed loan, or while a looped purchase is in progress, the authorizer permits the paired presale module to call transferLoan on the paired credit facility, whatever its ordinary role permissions. This permission covers only that function on that credit facility. The admin can remove or change the pair only while the presale module holds no unclaimed loan and no looped purchase is in progress. The presale module’s own setCreditFacility has the same condition and works only while the state is NotOpen.

After early access

Closing early access does not change any permission. At launch, PUBLIC_ROLE holds the curve’s sell, the credit facility’s repay and the staking entry points from the start. It does not hold the curve’s buy, which the launch route grants only to the presale module and the credit facility. After early access closes, the admin grants PUBLIC_ROLE three functions together in the market’s authorizer: From then on, any address can buy on the curve and borrow directly.

Parameters

The launch route sets the initial values from the launch configuration when it attaches the presale module, and admits the presale and credit facility pair. DEFAULT_ADMIN_ROLE can call every setter on this page. Authority lists every role.

Edge cases

  • One-entry schedule. With a base commission schedule of one entry, buyPresaleWithLoops reverts for every loop count, and only buyPresale is available.
  • Before the market is sealed. Until the market’s authorizer reports production as active (isProductionActive), it refuses setPresaleState and the curve’s buy for every caller, the admin included.
  • Fee at or above the deposit. A purchase whose provisional fee is at or above the deposit reverts.
  • Cap or slippage. A purchase that exceeds a cap or mints below the minimum amount out reverts in full, including the fee and the curve buy.
  • Multiplier changes. setInitialMultiplier and setDecayDuration have no state check. A change during Public applies to the next purchase.
  • Reopen. A reopen does not restart the decay, and it does not move the 730-day anchor of an existing purchase record.
  • End timestamp. After the end timestamp is reached, it cannot be changed or cleared, except while the state is NotOpen and no purchase record exists.
  • Frozen market. While the market is frozen, purchases, registration and claims revert.

Code

Source: house-markets/contracts at 0ebbc89a (branch integration/v12-post-qa).