# HyperPad — machine reference for agents

Authoritative data model for decoding state and constructing calls. Pair with
`abi.json` (call signatures) and `addresses.json` (deployment).

## Chain

- HyperEVM, chainId **999**, RPC `https://rpc.hyperliquid.xyz/evm`, native symbol **HYPE**.
- Explorer: `https://hyperevmscan.io`.

## Contracts

| contract | role | how you get its address |
|---|---|---|
| `LaunchpadFactory` | registry + launcher | from `addresses.json` (one per deployment) |
| `Sale` (clone) | one per launch | `factory.allSales(i)` or `SaleCreated` event |
| `LaunchToken` | the sold token | `getSaleData().saleToken` |
| `ClaimVault` | holds buyers' tokens | `Sale.claimVault()` |
| `LpFeeVault` | locks LP, splits fees | `factory.lpLocker()` |

## State machine

`state()` returns a uint8:

| value | name | meaning | buyer can |
|---|---|---|---|
| 0 | Pending | before `startsAt` | wait |
| 1 | Active | open for buys (`deposited` true, before `endsAt`, under `hardCap`) | **buy** |
| 2 | Ended | window closed or hard cap hit; not yet finalized | wait for finalize/cancel |
| 3 | Finalized | settled; if `claimsOpenAt!=0` claims are open | **claim** (once `claimsOpenAt`) |
| 4 | Cancelled | failed/cancelled OR escaped | **refund** (unless `escaped`) |

Transitions of interest:
- `Pending -> Active` at `startsAt` (if `deposited`).
- `Active -> Ended` at `endsAt`, on hard-cap sellout, or platform `endSaleEarly`.
- `Ended -> Finalized` via permissionless `finalize` / `finalizeAndCreateLiquidity` (needs soft cap or platform override).
- `Ended/Active -> Cancelled` via `cancel` (failed sale, permissionless once ended < soft cap; platform anytime pre-settlement).
- `Finalized` with `lpAdapter!=0` & `!lpCreated` for `>7d` -> `failPostFinalize` -> Cancelled (refunds).
- Any pre-settlement -> `escaped` (platform `emergencyEscape`): state reads Cancelled, but **not refundable**.

## Pricing modes & phases (read these BEFORE pricing a buy)

- **`pricingMode`** (field 29): `0 = FIXED` — caps in **quote** units, `tokens = amount *
  tokensPerQuoteUnit / quoteDecimalScale`. `1 = ORACLE_USD` — a Chainlink-compatible `priceFeed`
  (field 30) converts quote→USD at buy time; `tokenPriceUsd` (field 31, USD/token in the feed's
  decimals) sets the price; **all caps (soft/hard/min/max) are USD** in feed decimals; cumulative USD is
  `totalRaisedUsd` (field 32) and per-buyer USD is `contributedUsd(addr)`. Quote is still held/refunded
  in its own units (`totalRaised`/`contributed`). `tokens = usd(amount) * 10^saleTokenDecimals /
  tokenPriceUsd`, where `usd(amount) = amount * feed.answer / quoteDecimalScale`.
- **`getPhases()`** → `Phase[]` (empty = single-phase, use the flat fields). A `Phase` is
  `(uint64 startsAt, uint64 endsAt, uint256 tokensPerQuoteUnit, uint256 phaseCap, uint256 maxBuy,
  bytes32 merkleRoot)`. Caps are **cumulative** (high-water mark on `totalRaised`), strictly increasing,
  last `phaseCap == hardCap`. Resolve the **active phase** by `startsAt <= now <= endsAt` (closed
  intervals; the earlier phase wins a shared boundary second; a gap → `state == Pending`). Use the active
  phase's `tokensPerQuoteUnit`/`phaseCap`/`maxBuy`/`merkleRoot`; `minBuy` stays global. ORACLE_USD is
  single-phase only (its `getPhases()` is always empty).

## getSaleData() field order (33 fields)

Decode the returned tuple positionally (also in `abi.json:saleViewKeys`):

```
0  saleToken         address
1  quoteToken        address   (0x0 = native HYPE)
2  tokensPerQuoteUnit uint256  (FIXED price; 0 in ORACLE_USD)
3  softCap           uint256   (CAP UNIT: quote decimals if FIXED, USD feed-decimals if ORACLE_USD)
4  hardCap           uint256   (cap unit)
5  minBuy            uint256   (cap unit)
6  maxBuy            uint256   (cap unit)
7  startsAt          uint64    (unix s)
8  endsAt            uint64    (unix s)
9  merkleRoot        bytes32   (0x0 = open; single-phase only — multi-phase uses per-phase roots)
10 tgeBps            uint16    (0-10000)
11 cliffDuration     uint64    (s)
12 vestingDuration   uint64    (s)
13 lpAdapter         address   (0x0 = no auto-LP)
14 lpBps             uint16
15 state             uint8     (see table)
16 totalRaised       uint256   (QUOTE decimals — settlement/refund unit, both modes)
17 totalTokensSold   uint256   (sale-token decimals)
18 saleAllocation    uint256   (sale-token decimals)
19 finalizedAt       uint64
20 claimsOpenAt      uint64    (vesting anchor; 0 until claims open)
21 deposited         bool
22 cancelled         bool
23 lpCreated         bool
24 projectOwner      address
25 quoteDecimalScale uint256   (= 10^quoteDecimals; 1e18 for native)
26 platformFeeBps    uint256
27 escaped           bool
28 closedEarly       bool
29 pricingMode       uint8     (0 = FIXED, 1 = ORACLE_USD)
30 priceFeed         address   (ORACLE_USD quote→USD feed; 0x0 in FIXED)
31 tokenPriceUsd     uint256   (ORACLE_USD: USD/token in feed decimals; 0 in FIXED)
32 totalRaisedUsd    uint256   (ORACLE_USD: cumulative USD raised — the cap-progress unit; 0 in FIXED)
```

## Per-address reads

- `contributed(addr)` → quote contributed (quote decimals; the refund/withdraw unit, both modes).
- `contributedUsd(addr)` → ORACLE_USD: USD contributed (feed decimals; gates min/maxBuy in oracle mode).
- `purchased(addr)` → total sale tokens owed to addr (sale decimals).
- `claimed(addr)` → already claimed (sale decimals).
- `claimable(addr)` → unlocked-minus-claimed, claimable right now (sale decimals).

## Formulas

**FIXED (single-phase; multi-phase: substitute the ACTIVE phase's `tokensPerQuoteUnit` and `phaseCap`):**
```
quoteDecimalScale     = 10^quoteDecimals           (native: 1e18)
tokens_for(amount)    = amount * tokensPerQuoteUnit / quoteDecimalScale     (integer floor)
price_tokens_per_quote = tokensPerQuoteUnit / quoteDecimalScale
remaining_to_cap      = cap - totalRaised          (cap = hardCap, or active phaseCap if multi-phase)
remaining_per_wallet  = maxBuy - contributed[you]  (active phase maxBuy if multi-phase)
max_buyable_now       = min(remaining_to_cap, remaining_per_wallet)
```

**ORACLE_USD** (read the feed the same way the contract does; reverts on stale/incomplete/non-positive):
```
(roundId, answer, _, updatedAt, answeredInRound) = AggregatorV3(priceFeed).latestRoundData()
usd(amount)           = amount * answer / quoteDecimalScale                 (USD in feed decimals)
tokens_for(amount)    = usd(amount) * 10^saleTokenDecimals / tokenPriceUsd  (integer floor)
remaining_to_cap_usd  = hardCap - totalRaisedUsd
remaining_wallet_usd  = maxBuy  - contributedUsd[you]
# convert a USD room back to a max quote spend: room_usd * quoteDecimalScale / answer
```

Vesting unlocked at time `t` (with `e = t - claimsOpenAt`):
```
tge = purchased * tgeBps / 10000
if e < cliffDuration:                unlocked = tge
elif vestingDuration == 0:           unlocked = purchased
elif e >= cliffDuration + vesting:   unlocked = purchased
else: unlocked = tge + (purchased - tge) * (e - cliffDuration) / vestingDuration
claimable = unlocked - claimed
```

## Call quick-reference (buyer)

| goal | precondition | call | value |
|---|---|---|---|
| buy (native) | state==1, deposited, room | `buy(amount, proof)` | `amount` |
| buy (ERC20) | + allowance>=amount | `approve(sale, amount)` then `buy(amount, proof)` | 0 |
| claim | `claimsOpenAt!=0`, `claimable>0` | `claim()` | 0 |
| refund | `cancelled && !escaped`, `contributed>0` | `refund()` | 0 |
| open claims (stuck LP) | Finalized, `!lpCreated` | `createLiquidity()` | 0 |
| force-fail (LP timeout) | Finalized, `!lpCreated`, `>finalizedAt+7d` | `failPostFinalize()` | 0 |

## Reverts you will hit (and what they mean)

- `"Not active"` — sale not in Active state (too early/late, or sold out).
- `"Tokens not deposited"` — project hasn't funded yet; wait.
- `"Below min"` / `"Above max"` — per-wallet bounds on cumulative `contributed`.
- `"Hard cap"` — `amount` would exceed the cap; size down.
- `"Not whitelisted"` — the applicable root (single-phase `merkleRoot`, or the **active phase's** root)
  is set and your proof is missing/invalid (the leaf is bound to your address — you can't borrow one).
- `"Fee-on-transfer quote unsupported"` — the quote token taxes transfers; cannot buy.
- `"Oracle answer"` / `"Oracle round"` / `"Oracle stale"` — ORACLE_USD feed returned a non-positive
  answer / an incomplete round (`answeredInRound < roundId`) / a price older than `maxOracleDelay`.
  Transient (stale) — retry once the feed updates; persistent → the sale's feed is misconfigured.
- `"No active phase"` — multi-phase sale and `now` is in a gap between phases (state is Pending); wait.
- `"Nothing to claim"` — `claimable==0` right now (still in cliff, or fully claimed).
- `"Not cancelled"` — tried to refund a non-cancelled (e.g. escaped or live) sale.

## Decimals: the #1 agent mistake

Quote amounts (caps, contributions, budget) are in the **quote token's** decimals; token
amounts (allocations, claims) are in the **sale token's** decimals. They are usually
different (e.g. USDC 6 vs token 18). Always fetch `decimals()` on both and never hardcode 18.
