nfo · how it works · what can and cannot happen
Overview Lifecycle Fee split Market & oracle Bonds Buybacks Staking Idle funds Keepers Parameters Security & governance Risks Contracts FAQ

DOCUMENTATION

Lunarray is a two-contract protocol layer attached to a fixed-supply token launched on Pons v2 (Robinhood Chain). It is live from the bonding curve on and keeps running on Uniswap v4 after graduation. It turns the token's creator fees into ETH yield for stakers and into buybacks that pay out bonds.

Overview

Pons trades5% creator tax in ETH FeeSplitterharvest() Treasury3% (60% of each harvest) BondEngine2% (40% of each harvest) StakersETH rewards Buyback → cryptpays bonds

Lifecycle

  1. Deploy FeeSplitter with the treasury address and share (60%).
  2. Launch on Pons with creatorFeeRecipient = FeeSplitter and creatorTaxBps = 500. Both the tax and the fact that fees route through the splitter are visible on-chain from block one.
  3. Deploy BondEngine right after the launch. It reads the launch record from the Pons factory: the curve address, the pool fee and the tick spacing. The v4 pool id is derived on-chain from that pool key. The deployer wires FeeSplitter.setProtocol(engine) exactly once.
  4. Curve phase. Anyone calls start(): the first sample is the curve price and epochs begin. Fees are harvested, stakers earn ETH, and every epoch the engine buys back on the curve. Bonds open after minSamples epochs like on any other market.
  5. Graduation. Pons migrates liquidity to Uniswap v4 inside the buy that fills the curve. The engine notices curve.graduated() and reads and buys from the pool from then on. Same sample series, same epoch counter, no restart.
  6. Steady state. Every epoch a keeper calls poke(). Stakers accrue ETH, buybacks fill the crypt, bonds mature and are settled in order.

Fee split

Pons charges a base fee on every trade (currently 1%, of which 70% goes to the creator) plus the creator tax chosen at launch and immutable afterwards: 5% for LUNARRAY. All creator proceeds are credited in ETH to the recipient's balance in the Pons FeeEscrow, on the curve and on the pool alike.

DestinationShareMechanism
Treasury60% of every harvest (3% of each trade)Constant in FeeSplitter. Cannot be changed, cannot be paused.
StakersstakingShareBps of the protocol share (1% of each trade at default parameters)Added to a per-share accumulator the moment ETH arrives; claimable any time.
Buyback reservethe rest (1% of each trade at default parameters)releaseBps of the reserve is swapped for the token every epoch and locked in the crypt.
If nobody is staking when ETH arrives, the staker share is not lost: it falls through to the buyback reserve.

Market & oracle

priceSource() tells which market is live: 1 = the Pons curve (until curve.graduated() is true), 2 = the Uniswap v4 pool (once its sqrtPriceX96 is set), 0 = none.

Either way the value is stored as tokens per ETH once per epoch, in a ring buffer of up to 168 samples (7 days at 1-hour epochs). Graduation does not break the series: the pool opens at the price the curve ended on.

Target is the arithmetic mean of the last window samples. Discount is how far the current spot is below the target: discount = (spotTPE − targetTPE) / spotTPE, where a higher tokens-per-ETH means a cheaper token. When spot is at or above target the discount is zero and bonds are closed.

A spot price can be moved inside a block. The target cannot: it needs window epochs of sustained price to move. Bonuses are computed against the target, not against the last trade.

Bonds

bond(amount, minBonusBps) is available when the engine has at least minSamples samples, entries are not paused, and the discount is above zero.

  1. entryBurnBps of the amount is sent to the dead address.
  2. The remainder is the principal and goes to the crypt.
  3. The payout is principal × (1 + bonus), where bonus = maxBonusBps × min(discount, bandBps) / bandBps.
  4. The bond matures after vestEpochs epochs.

Bonds are paid strictly first-in, first-out by settle(n), which anyone can call. A bond is paid when it has matured and the crypt holds at least its payout. If the crypt is short, the queue waits for the next buybacks; nothing is paid out of order.

exit(id) cancels an unsettled bond at any time: the owner gets the principal minus penaltyBps, and the penalty stays in the crypt for the other bondholders.

Because payouts exceed principal, the crypt depends on inflows: buybacks funded by fees, entry penalties, and new principal. This is the same structure as coupon and bond systems in algorithmic protocols, and it is the risk described under Risks.

Buybacks

On every poke() the engine takes releaseBps of its ETH reserve and buys the token on the live market. Tokens received are added to the crypt and counted in totalBoughtBack.

Releasing a fixed fraction per epoch makes the buyback a predictable, continuous bid that cannot be front-run for size: the amount is public before the epoch ends.

Staking

stake(amount) deposits the token; unstake(amount) withdraws it at any time. Rewards are ETH, tracked with a per-share accumulator. earned(user) shows what is claimable, and claimRewards() sends it. Staking never pauses withdrawals: only new deposits can be paused by the guardian. sLUNARRAY is an optional liquid receipt on top: 1:1, transferable, rewards follow the balance.

Idle funds

The crypt and the ETH reserve belong to the protocol, not to any user. If the bond side is never used they would otherwise sit there forever. sweepIdle() lets the guardian move both to the treasury, but only when no bond is outstanding and no bond was opened, settled or exited for 30 days (IDLE_DELAY). The idle clock is public: idleFor().

What can never be swept: staked tokens, ETH already owed to stakers, and the payout of any open bond. Those are separate balances and the sweep does not touch them.

Keepers

Everything that keeps the protocol moving is permissionless:

FunctionWhenIncentive
FeeSplitter.harvest()whenever the escrow shows a balancenone needed; gas is negligible on Robinhood Chain
BondEngine.start()once, as soon as the curve is livenone
BondEngine.poke()once per epochcaller receives TIP_BPS of the reserve, capped at TIP_CAP
BondEngine.settle(n)after bonds maturenone; bondholders will call it
PonsFactory.createGraduatedPool(token)only if the graduating buy failed to create the poolnone; the reference keeper does it
# reference keeper (Hardhat project)
ENGINE=0x… SPLITTER=0x… LOOP=1 npx hardhat run scripts/3-start-and-poke.js --network robinhood

Parameters

NameDefaultMeaning
epochLength3600 sone sample and one buyback per hour (immutable)
window24target = mean of the last 24 samples
minSamples6bonds open 6 epochs after start()
maxBonusBps5000+50% payout at the full discount band
bandBps5000full bonus reached at 50% below target
entryBurnBps1001% of each bond entry burned
vestEpochs24bonds mature after 24 epochs
penaltyBps2000early exit returns principal minus 20%
releaseBps100010% of the ETH reserve bought back per epoch
stakingShareBps5000half of incoming ETH to stakers, half to the reserve
IDLE_DELAY30 daystime without any bond before idle funds can be swept (constant)

Changing any parameter requires proposeParams() by the guardian followed by a public executeParams() at least 48 hours later. Bounds are enforced on-chain (for example, entry burn ≤ 10%, penalty ≤ 50%).

Security & governance

ActorCanCannot
Guardianpause new bonds and stakes for ≤ 7 days per call; propose parameters with a 48h delay; sweep idle protocol funds after 30 days without bondsmove any staked token or owed ETH, block unstake / exit / settle / claims / poke, change addresses, the curve or the pool
FeeSplitter deployerset the protocol address oncechange shares, withdraw, set it twice
Anyoneharvest, start, poke, settle, executeParams after the delay

Risks

Contracts

ContractAddress
BondEnginesee config
FeeSplittersee config
Tokensee config
sLUNARRAYsee config
DevLock (dev allocation)see config
Pons curveimmutable curve() on the engine
Pons Factory0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e
Pons FeeEscrow0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e
Pons MemeHook0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044
Uniswap v4 PoolManager0x8366a39cc670b4001a1121b8f6a443a643e40951
Uniswap v4 StateView0xf3334192d15450cdd385c8b70e03f9a6bd9e673b

FAQ

Why are rewards paid in ETH and not in the token?

Because the fees arrive in ETH and because a reward that does not come from printing the token is the only kind that does not dilute holders.

Does anything happen before graduation?

Everything does. The engine starts on the curve: fees are harvested, stakers earn ETH, the reserve buys back on the curve every epoch and bonds open after the minimum number of samples. Graduation only changes where the price is read and where the buyback is executed.

Who decides when to pause?

The guardian address, and only for new entries. Anyone can always withdraw. A pause expires by itself after at most 7 days unless renewed.

Can the creator tax change?

No. It is set once in the Pons launch transaction and is immutable. The fee recipient can only be changed through the Pons factory with a 3-day timelock, which is public.

What if nobody ever bonds?

Stakers keep earning regardless. The crypt and the reserve would idle; after 30 days without any bond the guardian can move them to the treasury with sweepIdle(). Nothing that belongs to a user is part of that.

LUNARRAY · LIVE FROM THE CURVE · NOTHING IS MINTED · NO ADMIN KEYS OVER USER FUNDS · FEES SPLIT IN CODE · BUYBACK EVERY EPOCH · STAKERS PAID IN ETH · BOND THE DARK SIDE · LUNARRAY · LIVE FROM THE CURVE · NOTHING IS MINTED ·