Contracts overview
HartiiLabs is a pump.fun-style token launchpad running on Quai Network mainnet (chain 9, Cyprus-1) since 2026-07-05. Every launch is two EIP-1167 minimal-proxy clones — one ERC-20 token, one dedicated trading contract — deployed atomically by a single factory. There is no external DEX anywhere in the flow: the trading contract is the market for its token, before and after graduation.
Architecture, in words
┌─────────────────┐
creator ────────▶ │ TokenFactory │ singleton, not upgradable
(pays creation │ │
fee + picks │ launch(name, │
name/symbol/ │ symbol, hash, │
alloc%) │ allocBps) │
└────────┬─────────┘
│ same transaction:
┌───────────┴────────────┐
▼ ▼
clone (EIP-1167) clone (EIP-1167)
┌──────────────────┐ ┌──────────────────────┐
│ LaunchToken │ │ BondingCurve │
│ (this launch's │◀──▶│ (this launch's only │
│ ERC-20) │ │ market, forever) │
└──────────────────┘ └──────────────────────┘
│ │
PHASE 1: bonding curve (x*y=k,
virtual reserves) — buy()/sell()
│
sellout OR raise threshold hit
▼
PHASE 2: internal pool (x*y=k,
real reserves only) — SAME
buy()/sell(), same contract,
same Buy/Sell events- A creator calls
TokenFactory.launch()with a name, symbol, a metadata hash, and an optional creator allocation (0–20% of supply), paying the flatcreationFeein QUAI. - The factory clones its
LaunchTokenandBondingCurveimplementations via plainCREATE(notCREATE2— seetoken-factory.mdfor why), then initializes both clones in the same transaction. There is no window where an uninitialized clone could be front-run. LaunchToken.initialize()mints the entire fixed supply exactly once: the non-creator portion goes to the newBondingCurveclone, and the creator's allocation (if any) goes to the creator, locked under a vesting schedule.- The
BondingCurveclone is now the only place this token trades. It starts in the bonding phase: a constant-product (x*y=k) curve priced off virtual reserves plus whatever real QUAI has actually been paid in. - When the curve's sellable supply sells out (the normal path) — or, as a belt-and-braces guard,
once a configured QUAI-raised threshold is crossed — the curve graduates: it seeds an
internal constant-product pool from its real (non-virtual) reserves plus a migration
token reserve set aside at launch, and freezes the bonding-phase accounting.
buy()/sell()keep working on the exact same contract and address, now routed through pool math. Indexers can follow a token's whole life from the sameBuy/Sellevent stream with no address change. - Every trade (curve phase or pool phase) takes a fee in QUAI, split 50/50 between the token's creator and the platform treasury, each pulled independently.
Full field-by-field references: Token factory, Bonding curve, Launch token. Integration examples: Integrating with quais.
Live mainnet addresses (chain 9, Cyprus-1)
Read from AGENTS.md in the hartii-labs repo and confirmed live via quai_getCode /
quai_call on 2026-09-23.
| Contract | Address | Role |
|---|---|---|
| TokenFactory | 0x001AF1BbB40807fcb99C9Eeaa49dF5E91e7Efd42 | Singleton. Launches every token + curve pair. |
| LaunchToken implementation | 0x0059af4b7441e15E06e4686bd2f4CC5dfF5AAA0A | Clone target for every launched token. Never itself initializable. |
| BondingCurve implementation | 0x0062D75A096E67FEF48A8C5a9fD9094d2BEa9D14 | Clone target for every launch's trading contract. Never itself initializable. |
quai_getCode returned non-empty bytecode for all three addresses at block 10249966
(2026-09-23): factory 8,556 bytes, token implementation 7,664 bytes, curve implementation
14,424 bytes.
The treasury/admin wallet is not printed here — read it live via TokenFactory.owner() /
TokenFactory.treasury() (they currently return the same address) or per-curve via
BondingCurve.owner() / BondingCurve.treasury().
What is immutable vs. owner-settable
| Contract | Immutable (set once, no setter exists) | Owner-settable |
|---|---|---|
TokenFactory | tokenImplementation, curveImplementation (both immutable, set in the constructor) | owner, treasury, creationFee, curve defaults for future launches (virtualQuaiReserve, virtualTokenReserve, totalSupplyPerToken, graduationRaiseWei, creatorLockDuration via setCurveParams), curveSellableBps |
BondingCurve (per launch) | token, creator, factory, virtualQuaiReserve, virtualTokenReserve, curveSupply, migrationTokenReserve, graduationRaiseWei, launchBlock — all frozen at initialize(), copied from the factory's settings at launch time | owner (that curve's own owner, initialized to the factory owner but independently transferable), treasury (that curve's own, independently settable), feeBps (capped at MAX_FEE_BPS = 300 = 3%) |
LaunchToken (per launch) | Everything — name, symbol, totalSupply (only ever decreases via burn()), creator, creatorAllocation, lockedUntil. No owner/admin role exists on this contract at all. | Nothing |
Trust model — exactly what the owner can and cannot do
The owner is the address returned by TokenFactory.owner() (currently equal to treasury() —
read live, not printed here as a literal per house convention).
Can:
- Change
creationFeeto any value, with no minimum or maximum (setCreationFee). - Change the curve defaults applied to future launches (
setCurveParams,setCurveSellableBps). - Withdraw the factory's accumulated creation fees to
treasury(withdrawFees). - Per curve: change that curve's trading fee, capped at 3% (
setFeeBps), change that curve's fee destination address (setTreasury), and transfer that curve's own ownership. - Per curve: withdraw the treasury's half of that curve's accrued trading fees
(
withdrawTreasuryFees) — pulled totreasury, never anyone else. - Per curve: sweep tokens sent directly to the curve's address by mistake (dust/donations),
via
sweepDonatedTokens— see the accounting guard below. - Transfer factory ownership or any curve's ownership to a new address.
Cannot — structurally, not just by policy:
- Mint additional token supply.
LaunchTokenhas nomint()function anywhere in its ABI; the full fixed supply is minted exactly once, insideinitialize(). - Withdraw real bonding-curve or pool reserves. There is no
withdrawLiquidity/removeLiquidity/ owner-drain function anywhere inBondingCurve's ABI. The only way real QUAI or real tokens leave the pool is through abuy()orsell()swap. - Redirect a creator's fee share. The creator side of every fee split is hardcoded to that token's
creatoraddress (set once at curveinitialize()) — there is no setter for it. - Pause trading, blacklist a wallet, or block a specific transfer.
LaunchToken'stransfer/transferFromare the plain textbook ERC-20 implementation with no hook point for any of that;BondingCurvehas no pause switch. - Re-initialize a clone. Both
LaunchToken.initialize()andBondingCurve.initialize()are guarded by a one-time_initializedflag. - Sweep real reserves via
sweepDonatedTokens. That function computesowed(the tokens the curve is legitimately supposed to be holding — unsold curve supply + migration reserve pre-graduation, or the live pool reserve post-graduation) and only ever sweeps the balance above that figure; it reverts ("Nothing to sweep") if there is no excess. - Retroactively change an already-launched curve's virtual reserves, curve supply, migration
reserve, or graduation threshold — no setter exists for any of them on
BondingCurve.
Security properties
- No withdraw-liquidity path. Confirmed above: neither
BondingCurvenorTokenFactoryexposes any function that moves real reserves anywhere other than through a swap or a fee pull to its designated recipient. - Explicit reserve accounting, never
balanceOf/address(this).balance.realQuaiReserve,tokensSold,poolQuaiReserve,poolTokenReserveare all separate state variables. A direct QUAI or token transfer straight to the curve's address has zero effect on quoted price — it just sits there until the owner sweeps the token side (never the QUAI side; seeBondingCurve.sol'sreceive()comment — there is deliberately no QUAI-donation sweep in v1). - Checks-effects-interactions + pull-payment fallback ("safe-pay") on every payout. Every
function that pays out QUAI (
withdrawFees,withdrawTreasuryFees,withdrawCreatorFees, refunds insidebuy(),sell()'s payout) zeroes the relevant balance before the external call, and falls back to creditingpending[recipient](claimable via a separatewithdraw()) if the direct send fails, rather than reverting the whole trade. nonReentrantguard onBondingCurve.buy,sell,withdrawTreasuryFees,withdrawCreatorFees,withdraw, andsweepDonatedTokens.TokenFactoryhas no reentrancy modifier at all — itswithdrawFees/withdraware safe by CEI alone (balance zeroed before the external call), since the factory does not call into curve or token contracts during a payout.- Hard fee cap.
feeBpscannot exceedMAX_FEE_BPS = 300(3%) on any curve — enforced bysetFeeBps'srequire, so a fee can never be raised into a de facto rug. - Hard creator-allocation cap.
creatorAllocBpscannot exceedMAX_CREATOR_ALLOC_BPS = 2000(20%) at launch — enforced byTokenFactory.launch(). - Anti-snipe caps. For the first
SNIPE_WINDOW_BLOCKS(20) blocks after a curve's launch block, each wallet is capped atPER_WALLET_CAP_BPS(1%) ofcurveSupply; the creator specifically is capped atCREATOR_CAP_BPS(2%) ofcurveSupplyon the launch block only. See Bonding curve for the exact enforcement. - Structurally honeypot-proof token.
LaunchTokenhas no fee-on-transfer, no blacklist, no pause, no owner-only transfer hook — a creator cannot quietly disable sells because there is no hook point in the contract to add one. - Pool never fully drains.
_poolBuy/_poolSellboth require strictly-positive reserve headroom remain on both sides after a trade ("Pool reserve floor"), so a single trade can never brick the pool with a divide-by-zero on the next quote.
Audits / tests
No third-party audit report was found in the hartii-labs repository or in AGENTS.md. The
verification evidence available is:
- The Hardhat test suite:
cd contracts && npx hardhat test— 50 tests acrosscontracts/test/*.cjs(perhartii-labs/CLAUDE.md), including fuzzed trade sequences, graduation via both triggers, re-graduation-impossibility, snipe-cap enforcement, and pool-reserve-floor behavior forBondingCurvespecifically. - A mainnet deploy rehearsal script (
contracts/scripts/rehearse-launch.cjs) that launches a throwaway token and validates clone zone-safety on Cyprus-1. - This page's own on-chain reads (
quai_getCode,quai_call) against the live addresses above.