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 callsbuyPresale with a deposit of collateral. The presale module:
- computes the early-access fee on the deposit (see The fee)
- reverts if the fee is at or above the deposit
- buys on the curve with the rest of the deposit, the execution budget
- 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
- adjusts the fee to the collateral the curve consumed, sends the fee to its fee treasury, and refunds any unspent collateral to the buyer
- creates a purchase record for the buyer and keeps the minted tokens. The code calls the record a
Positionand numbers it withpositionId
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, exceptNotOpen→Closed - a reopen from
ClosedtoWhitelistorPublic
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%). where:- is the deposit, in collateral
- is the loop count of the purchase: 0 for
buyPresale, forbuyPresaleWithLoopswith rounds - is the base commission for loop count , in bps (
getBaseCommissionBps) - is the fee multiplier in bps, where 10,000 is 1× (
getMultiplier). DuringWhitelist, is 10,000 - is the rate charged, in bps
- is the collateral that left the presale module during the purchase
- is the amount used for the fee: for
buyPresale; forbuyPresaleWithLoops, the smaller of the execution budget and plus the net proceeds of the loans the purchase created - equals the provisional fee when equals the execution budget
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 toPublic, unless the admin has set a start time with setStartTime. The multiplier falls fastest at the start of the decay.
where:
- is the initial multiplier in bps (
getInitialMultiplier) - is the decay duration in seconds (
getDecayDuration) - is the decay start time (
getStartTime) - is the block timestamp
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 borrow-and-buy rounds through the market’s credit facility. Credit and looping explains a round. The presale module:
- computes the fee with base commission
- calls the credit facility’s
buyAndBorrowwith the execution budget and rounds. The presale module is the borrower, so it holds every loan the rounds create - buys on the curve with any collateral the credit facility returns
- creates a purchase record with one tranche per loan and the tokens from the final buy as direct tokens
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 of any looped purchase record uses breakpoint . After early access closes, tranche becomes claimable when one of these holds:- the reference price is at or above breakpoint
- the end timestamp has been reached
- 730 days have passed since the first explicit close after the purchase record was created
setPresaleState, no end timestamp is set, and the reference price is 1.15.
Claiming
After early access ends, the owner of a purchase record callsclaimAll(positionId). In one call the presale module:
- transfers the purchase record’s direct tokens to the owner, if they have not been claimed
- 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
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,
buyPresaleWithLoopsreverts for every loop count, and onlybuyPresaleis available. - Before the market is sealed. Until the market’s authorizer reports production as active (
isProductionActive), it refusessetPresaleStateand the curve’sbuyfor 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.
setInitialMultiplierandsetDecayDurationhave no state check. A change duringPublicapplies 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
NotOpenand 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).