Hartii developer docs

HartiiLabs

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

text
                    ┌─────────────────┐
  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
  1. A creator calls TokenFactory.launch() with a name, symbol, a metadata hash, and an optional creator allocation (0–20% of supply), paying the flat creationFee in QUAI.
  2. The factory clones its LaunchToken and BondingCurve implementations via plain CREATE (not CREATE2 — see token-factory.md for why), then initializes both clones in the same transaction. There is no window where an uninitialized clone could be front-run.
  3. LaunchToken.initialize() mints the entire fixed supply exactly once: the non-creator portion goes to the new BondingCurve clone, and the creator's allocation (if any) goes to the creator, locked under a vesting schedule.
  4. The BondingCurve clone 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.
  5. 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 same Buy/Sell event stream with no address change.
  6. 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.

ContractAddressRole
TokenFactory0x001AF1BbB40807fcb99C9Eeaa49dF5E91e7Efd42Singleton. Launches every token + curve pair.
LaunchToken implementation0x0059af4b7441e15E06e4686bd2f4CC5dfF5AAA0AClone target for every launched token. Never itself initializable.
BondingCurve implementation0x0062D75A096E67FEF48A8C5a9fD9094d2BEa9D14Clone 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

ContractImmutable (set once, no setter exists)Owner-settable
TokenFactorytokenImplementation, 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 timeowner (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 creationFee to 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 to treasury, 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. LaunchToken has no mint() function anywhere in its ABI; the full fixed supply is minted exactly once, inside initialize().
  • Withdraw real bonding-curve or pool reserves. There is no withdrawLiquidity / removeLiquidity / owner-drain function anywhere in BondingCurve's ABI. The only way real QUAI or real tokens leave the pool is through a buy() or sell() swap.
  • Redirect a creator's fee share. The creator side of every fee split is hardcoded to that token's creator address (set once at curve initialize()) — there is no setter for it.
  • Pause trading, blacklist a wallet, or block a specific transfer. LaunchToken's transfer / transferFrom are the plain textbook ERC-20 implementation with no hook point for any of that; BondingCurve has no pause switch.
  • Re-initialize a clone. Both LaunchToken.initialize() and BondingCurve.initialize() are guarded by a one-time _initialized flag.
  • Sweep real reserves via sweepDonatedTokens. That function computes owed (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 BondingCurve nor TokenFactory exposes 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, poolTokenReserve are 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; see BondingCurve.sol's receive() 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 inside buy(), sell()'s payout) zeroes the relevant balance before the external call, and falls back to crediting pending[recipient] (claimable via a separate withdraw()) if the direct send fails, rather than reverting the whole trade.
  • nonReentrant guard on BondingCurve.buy, sell, withdrawTreasuryFees, withdrawCreatorFees, withdraw, and sweepDonatedTokens. TokenFactory has no reentrancy modifier at all — its withdrawFees/withdraw are 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. feeBps cannot exceed MAX_FEE_BPS = 300 (3%) on any curve — enforced by setFeeBps's require, so a fee can never be raised into a de facto rug.
  • Hard creator-allocation cap. creatorAllocBps cannot exceed MAX_CREATOR_ALLOC_BPS = 2000 (20%) at launch — enforced by TokenFactory.launch().
  • Anti-snipe caps. For the first SNIPE_WINDOW_BLOCKS (20) blocks after a curve's launch block, each wallet is capped at PER_WALLET_CAP_BPS (1%) of curveSupply; the creator specifically is capped at CREATOR_CAP_BPS (2%) of curveSupply on the launch block only. See Bonding curve for the exact enforcement.
  • Structurally honeypot-proof token. LaunchToken has 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/_poolSell both 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 across contracts/test/*.cjs (per hartii-labs/CLAUDE.md), including fuzzed trade sequences, graduation via both triggers, re-graduation-impossibility, snipe-cap enforcement, and pool-reserve-floor behavior for BondingCurve specifically.
  • 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.
Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.