Documentation
How EarnPad works.
What a vault does with a deposit, how the coin allocation is worked out, what happens inside the launch transaction, and every limit the factory refuses to bend. Written against the deployed contracts.
Overview
A coin normally launches by taking money off the people who buy it. On EarnPad the money is never taken. Depositors park USDG behind a coin, the USDG earns interest in spUSDG, and the interest is what buys the coin's liquidity. The principal is untouched by every code path in the launch, and it can be withdrawn in any block, including after the coin has already minted.
Two words are used throughout and mean different things. A vault is one launch:
an EarnPool contract deployed by the factory, holding deposits and a
target. A coin is the LaunchToken that vault mints once it hits
that target. One vault mints at most one coin, once, and never again.
Lifecycle
A vault is in exactly one of three phases. It starts in Funding and leaves it once, in one direction, for good.
| Phase | Enum | What is possible |
|---|---|---|
| Funding | 0 | Deposits open. Interest accrues toward the target. Anyone can withdraw principal. Either launch() or cancel() ends it. |
| Launched | 1 | The coin exists and trades. Deposits are shut. Withdrawals and claims stay open forever. |
| Cancelled | 2 | No coin was minted. The interest earned is handed back to depositors alongside their principal. |
The moment a vault leaves Funding it records closedAt, and the clock that
drives allocation stops at that timestamp. Nothing after the close changes anyone's
share of the coin.
Depositing and withdrawing
deposit(assets) takes USDG, six decimals, and puts it into spUSDG in the
same transaction. It is earning before the block closes. Your balance is recorded in
principal[you], which is the exact number of USDG the vault owes you back
and the only number a withdrawal reads.
Every vault has a per-depositor cap, maxPerAddress, and the factory
refuses to deploy a vault without one. A deposit that would carry your principal over
the cap reverts. This is what stops one address quietly owning a launch.
Withdrawing
withdraw(assets) works in every phase, for the full amount you put in,
with no lock and no notice period. While the vault is still funding it pulls exactly
your principal out of spUSDG and leaves the interest behind, because the interest
belongs to the launch. After the close the remaining spUSDG position is frozen against
the remaining principal, so interest that accrues after the launch leaves with you
rather than piling up in the contract.
How allocation is worked out
Allocation is principal-seconds, not a balance at a snapshot. One USDG parked for one second is one unit of weight. Your share of the depositor pot is your weight over the total weight at the close.
This is not an arbitrary formula. Every deposit sits in the same venue earning the same rate, so principal-seconds is exactly proportional to the USDG of interest each depositor contributed. The people who paid for the liquidity get the coins, in proportion to what they paid.
It also makes sniping pointless. A whale who deposits the day before a launch fires buys one day of weight against everybody else's months. There is no snapshot to time.
Reading it live
weightOf(who)is your principal-seconds as of this block.totalWeightNow()is the denominator as of this block.allocationOf(who)is the coins you would get if the vault launched now.
All three keep moving while the vault is funding, because other people are still
earning weight beside you. They freeze at closedAt.
Sizing a launch
This is the part creators get wrong. The target is denominated in interest, not
in deposits, so the capital a vault needs is much larger than the target. At a rate
r over a window of t years, the deposits required are
target / (r × t).
reading the live spUSDG rate
| Target | Window | Deposits it needs |
|---|---|---|
| $1,000 | 90 days | ... |
| $5,000 | 180 days | ... |
| $20,000 | 365 days | ... |
| $100,000 | 365 days | ... |
A vault that asks for more interest than its depositors can produce before the deadline never mints anything. It is not a loss for anyone, because cancelling returns principal and interest both, but it is months spent for nothing. Pick a target the capital you can actually attract will reach.
Opening a vault
EarnPad.create(cfg) is permissionless and costs the creator nothing but
gas. The caller is recorded as the creator; passing somebody else's address in is not
possible. Every field below is fixed at deployment and can never be changed
afterwards.
| Field | Limit | Notes |
|---|---|---|
name | 1 to 64 bytes | The coin's name. |
symbol | 1 to 11 bytes | The coin's ticker. |
description | 600 bytes | Stored on chain, not in an index this project controls. |
image | 200 bytes | https:// or ipfs://. |
website | 200 bytes | Optional. |
xHandle | 40 bytes | Handle only, no URL. |
telegram | 40 bytes | Handle only, no URL. |
yieldTarget | above zero | USDG of interest that fires the launch. See sizing. |
deadline | 7 to 365 days out | After it passes, anyone can cancel. |
creatorBps | 1,000 max | 10% of supply is the ceiling on the creator's cut. |
depositorBps | see below | Supply reserved for the people who funded it. |
maxPerAddress | required | Per-depositor principal cap. Zero is rejected. |
devBuy | at most the cap | The creator's own opening deposit, pulled in the same transaction. Optional. |
The four refusals
These are the reason the factory exists. They are in code rather than in a document, so a vault that violates one cannot be deployed at all:
- The creator can never take more than 10% of supply.
- At least 40% of supply has to end up in the liquidity position.
- Every vault has a per-depositor cap, so one address cannot own a launch.
- The deadline is bounded at both ends, so capital is never committed open-endedly.
The split is checked as depositorBps + creatorBps ≤ 10,000 with at
least 4,000 left over for liquidity. A launch that wants different numbers has to
deploy its own factory, and anyone reading a vault can see which factory made it.
maxPerAddress, earns weight the same way, and can be withdrawn
whenever the creator likes. It buys no privilege.
The launch transaction
launch() is permissionless. Once the interest is there, nobody has to be
asked and nobody can hold it back, including the creator. It reverts unless
yieldAccrued() has reached the target plus a dust reserve of 0.001 USDG.
Inside one transaction, in this order:
- Funding closes and the weight clock stops.
- The surplus above principal is redeemed out of spUSDG. It is counted in shares rather than assets, so rounding can never leave the vault a unit short of the principal it owes.
- The coin is deployed with a fixed supply of 1,000,000,000. The creator's cut transfers out immediately; the rest stays with the vault.
- The whole raise is sold for ETH through the ETH/USDG pool.
- A Uniswap v4 pool is opened for the coin against that ETH, at the price the raise implies, and the liquidity is minted into it.
- Anything the mint could not absorb is credited to the depositors rather than kept.
The opening price cannot be known in advance, because it depends on how much ETH the raise actually buys. The pool is priced and initialised after the swap has settled, inside the same callback.
The market that opens
A coin quoted in USDG is not a coin anybody trades, so the raise is sold for ETH first
and the market is opened against that. Native ETH sorts below every token address, so
it is always currency0.
| Property | Value |
|---|---|
| Venue | Uniswap v4 |
| Quote | Native ETH |
| Fee tier | 1% |
| Tick spacing | 200 |
| Range | Full |
| Hooks | None |
| Route to ETH | ETH/USDG, 0.01%, spacing 1 |
The fee tier is 1% deliberately. A new coin is volatile, and the fee stream is part of what depositors are paid, so it is set high rather than low.
The liquidity cannot be pulled
The position is minted to the vault contract and there is no function that removes it.
No owner, no timelock, no withdrawal path. The only call that touches the position
after launch is collect(), which harvests fees with a zero-delta modify
and cannot change the liquidity.
Getting paid
A depositor in a launched vault is owed four separate things:
| What | Asset | Paid by |
|---|---|---|
| Principal | USDG | withdraw(), any block, any phase |
| Coin allocation | The coin | claim(), once |
| Trading fees | ETH | claim(), repeatedly |
| Trading fees | The coin | claim(), repeatedly |
The pool earns fees in both of its currencies, so depositors are paid in both. These are tracked in separate accumulators, because mixing a cancelled vault's USDG refund with a launched vault's ETH and coin fees would lose track of which is which.
collect()
Fees sit in the Uniswap position until somebody sweeps them into the vault's claim
accounting. collect() does that and is permissionless, because people who
are owed fees should not need anyone's cooperation to be paid. Calling
claim() pays out whatever has been swept so far; calling
collect() first sweeps everything up to the current block.
claim() is safe to call as often as you like. The coin allocation only
ever pays once; the fee streams pay whatever has accrued since your last call.
When a launch fails
cancel() is open to the creator at any time, and to anyone at all once
the deadline has passed. It closes funding and hands the accrued interest back to the
depositors, split on exactly the weights the coin allocation would have used.
So the failure case returns principal and interest. A depositor in a vault that never launches ends up with more USDG than they put in. What they spent is time and the option to have earned that interest somewhere else.
Function reference
EarnPad, the factory
| Function | Who | What |
|---|---|---|
create(cfg) | anyone | Deploys a vault and returns its address. |
poolCount() | view | How many vaults exist. |
pools(i) | view | The vault at index i. |
isPool(addr) | view | Whether this factory deployed that vault. |
venue(), asset() | view | spUSDG and USDG, read off the venue rather than assumed. |
EarnPool, one per vault
| Function | Who | What |
|---|---|---|
deposit(assets) | anyone | Park USDG. Funding phase only, capped per address. |
withdraw(assets) | depositor | Take principal back. Any phase, any block. |
launch() | anyone | Mint the coin and open its market. Needs the target met. |
cancel() | creator, or anyone after the deadline | Give up and return the interest to depositors. |
collect() | anyone | Sweep the position's trading fees into claims. |
claim() | depositor | Take the allocation and any fees owed. |
yieldAccrued() | view | USDG of interest standing behind the launch right now. |
allocationOf(who) | view | Coins that address would get at current weights. |
feesOwed(who) | view | Unclaimed USDG, ETH and coin fees. |
phase() | view | 0 funding, 1 launched, 2 cancelled. |
Addresses
Robinhood Chain, id 4663.
A vault deployed by this factory answers isPool(addr) with true. Anything
claiming to be an EarnPad vault that fails that check was not made here.
Risk
The contracts are unaudited. 42 tests pass, six of them against live mainnet state, and no third party has reviewed the code. That is the largest risk here and it is not reduced by anything else on this page.
Venue risk
Deposits sit in spUSDG, not in EarnPad. If that vault loses value, is paused, or stops allowing withdrawals, deposits are affected regardless of what EarnPad's own code does. The rate it pays is also not fixed, so how long a launch takes is not fixed either.
The mechanism is slow
Interest-funded liquidity needs a lot of capital parked for a long time. See sizing a launch. A vault whose target is out of reach simply never mints.
Launch price depends on routing
The raise is sold for ETH through a single pool, and the coin's opening price is set by what that swap returns. A large raise against thin routing liquidity gets a worse price, and the coin opens accordingly.
Configuration is permanent
Target, deadline, splits and caps are immutable from the moment a vault is deployed. There is no function to lower a target or extend a deadline. A vault configured badly stays configured badly until somebody cancels it.
The creator holds up to 10%
The creator's cut transfers to them at launch with no vesting. It is capped at 10% of
supply and is visible on chain before you deposit, in creatorBps.