Token program
Mints, holdings and the token instructions
A HELIX token is a Mint account; each owner's balance of it is a Holding. Both are accounts of the HELIX program, laid out at fixed offsets. Every transfer, mint and burn runs through the mint's hook, if it has one, and logs an event.
Every HELIX account starts with a one-byte kind. Fields are packed little-endian with no padding beyond what the layout declares, so the SDK's decoders (decodeMint, decodeHolding, …) read them at fixed offsets. The token standard uses the mint and the holding; the DEX, the launchpad and the bridge use the rest.
| Account | Address | Bytes | Kind |
|---|---|---|---|
| Config | ["config"] | 240 | 1 |
| Mint | A keypair account that signs its creation; for a bridged-in token, ["wrapped", spl_mint] | 448 | 2 |
| Holding | ["holding", mint, owner] | 184 | 3 |
| Pool | ["pool", mint] | 216 | 4 |
| Launch | ["launch", mint] | 320 | 5 |
| Position | ["position", pool, owner] | 96 | 6 |
| Wrapper | ["wrapper", spl_mint] | 192 | 7 |
| Mirror | ["mirror", helix_mint] | 160 | 8 |
The mint keeps its supply, its four authorities, its hook and its metadata. There is no separate metadata account: name, symbol and URI are on the mint.
| Field | Offset | Type | Meaning |
|---|---|---|---|
decimals | 1 | u8 | At most 18. Launches use 6. |
flags | 2 | u8 | 1: the built-in kit is the hook. 2: created by a launch. |
hook_flags | 6 | u16 | Which callbacks run and what they may answer (see flags). Zero when there is no hook. |
supply | 8 | u64 | Tokens in existence, in base units. |
max_supply | 16 | u64 | Zero: no cap. Otherwise mint_to refuses to pass it. |
mint_authority | 24 | address | May mint. Zero: nobody, for good. |
freeze_authority | 56 | address | May freeze and thaw holdings. Zero: nobody. |
hook_authority | 88 | address | May change the hook and its flags. Zero: nobody. |
metadata_authority | 120 | address | May change name, symbol and URI. Zero: nobody. |
hook_program | 152 | address | Zero: no hook. The HELIX program id: the built-in kit (launches only). Anything else: an external hook. |
created_at | 184 | i64 | Unix time of creation. |
name | 200 | [u8; 32] | UTF-8, length in byte 3. |
symbol | 232 | [u8; 16] | UTF-8, length in byte 4. |
uri | 248 | [u8; 200] | UTF-8, length (u16) at offset 192. |
One owner's balance of one mint, at the PDA ["holding", mint, owner]. There is exactly one per pair, so there is nothing like an “associated” account to look up. Anyone can create it for anyone, paying its rent: about 0.00217 SOL (2,171,520 lamports for 184 bytes at Solana's current rate), returned by close_holding.
| Field | Offset | Type | Meaning |
|---|---|---|---|
frozen | 2 | u8 | Non-zero: the holding can neither send nor receive, nor be credited a hook’s cut. |
mint | 8 | address | The token. |
owner | 40 | address | The wallet, or a pool for a pool’s own holding. |
amount | 72 | u64 | Balance in base units. |
delegate | 80 | address | One delegate at a time. Zero: none. |
delegated_amount | 112 | u64 | What the delegate may still move or burn. |
hook_data | 120 | [u8; 64] | Belongs to the mint's hook; only the hook can change it (see hook data). |
The first byte of instruction data is the tag; the arguments follow, little-endian. (s) marks a signer, (w) a writable account. …hook is the mint's hook accounts: nothing for a mint without a hook, [launch] for the built-in kit, and [hook_program, hook_signer, …extras] for an external hook. The SDK's hookAccounts() builds them.
| Tag | Instruction and accounts | What it does |
|---|---|---|
| 10 | create_mintpayer (s, w), mint (s, w), system | Creates a mint at a fresh keypair address. Arguments: decimals, hook flags, max supply, the four authorities, hook program, name, symbol, URI. |
| 11 | create_holdingpayer (s, w), holding (w), mint, owner, system | Creates owner’s holding. Idempotent: if it already exists for that mint and owner, it does nothing, so it is safe to prepend to any transaction. |
| 12 | transfer(amount)authority (s), source (w), destination (w), mint, …hook | Moves tokens through the hook. The authority is the owner or its delegate. Context: Plain. |
| 13 | mint_to(amount)mint_authority (s), mint (w), destination (w), …hook | Mints into a holding through the hook, up to the max supply. |
| 14 | burn(amount)authority (s), holding (w), mint (w), …hook | Burns from a holding through the hook. Owner or delegate. |
| 15 | approve(amount)owner (s), holding (w), delegate | Sets the delegate and its allowance, replacing any earlier one. |
| 16 | revokeowner (s), holding (w) | Clears the delegate. |
| 17 | set_frozen(frozen)freeze_authority (s), mint, holding (w) | Freezes or thaws a holding. |
| 18 | close_holdingowner (s), holding (w), destination (w), mint | Closes an empty holding and sends its rent to destination. |
| 19 | set_authority(kind, new)current (s), mint (w) | Hands an authority over. kind: 0 mint, 1 freeze, 2 hook, 3 metadata. A zero address revokes it for good. |
| 20 | set_hook(program, flags)hook_authority (s), mint (w) | Changes the hook and its flags. A zero program removes the hook. |
| 21 | update_metadata(name, symbol, uri)metadata_authority (s), mint (w) | Rewrites the metadata. |
| 22 | write_hook_data(data)hook_self (s), mint, holding (w) | Lets the mint's external hook write a holding's 64 bytes outside a callback (see Hooks). |
Rules worth knowing
create_mintrefuses more than 18 decimals, unknown flag bits, and the HELIX program id as hook: the built-in kit comes only with a launch. With no hook program, the flags are stored as zero.- Name, symbol and URI are at most 32, 16 and 200 bytes. A launch also refuses an empty name or symbol.
create_holdingworks even if someone sent lamports to the holding's address in advance, so nobody can block a holding from being created.close_holdingneeds a zero balance. On a kit token it also needs no unclaimed rewards; on a token whose hook writes hook data, the 64 bytes must be all zero.- A transfer to the same holding is refused; so is an amount of zero.
- A delegate's transfers and burns come out of its allowance; a transfer that uses it up clears the delegate.
The DEX and launchpad instructions are on DEX and Launches; the admin's on Security.
- HELIX checks that source and destination differ, that the amount isn't zero, that both holdings belong to this mint and neither is frozen, and that the source holds the amount (and a delegate its allowance).
- If the mint's flags ask for it, HELIX calls
before_transfer. The hook may refuse (the whole transaction fails) or answer with cuts and new hook data. - The source loses the full amount. The destination gains the amount less the cuts; each cut is credited to the holding the hook named. Hook data from the answer is written.
- If the flags ask for it, HELIX calls
after_transferwith the balances after the move anddeltaset to the cuts' total. - HELIX logs a Transfer event with the amount, the cuts and the context.
Mints and burns follow the same pattern with before_mint/after_mint and before_burn/after_burn; they can't answer cuts. For a mint the mint account stands in for the source; for a burn, for the destination.
HELIX logs an event as program log data: one Program data: <base64> line per event, whose first byte is the kind and whose fields follow at fixed widths (addresses 32 bytes, amounts u64, times i64). parseEvents(logs) in the SDK decodes them, and keeps only data logged while HELIX itself was executing, so a hook can't forge a HELIX event from inside a callback.
| Kind | Event | Fields | Logged by |
|---|---|---|---|
| 1 | Transfer | mint, from owner, to owner, authority, amount, cut, context | every transfer, swaps and liquidity included |
| 2 | MintTo | mint, to owner, amount | mint_to, create_launch |
| 3 | Burn | mint, from owner, amount | burn, the burn rule of a trade, graduation |
| 4 | Swap | mint, trader, side, SOL, tokens, protocol fee, creator fee, holder fee, sniper fee, burned, SOL reserve, token reserve, mode, time | swap |
| 5 | LaunchCreated | mint, creator, pool, hook program, rule bits, the six fee and wallet rules, creator unlock, early window end, early unlock, time | create_launch |
| 6 | Graduated | mint, AMM SOL, AMM tokens, tokens burned, time | the buy that sells the curve out |
| 7 | PoolCreated | mint, creator, tokens, SOL, LP | create_pool |
| 8 | Liquidity | mint, owner, removed, SOL, tokens, LP | add_liquidity, remove_liquidity |
| 9 | RewardsClaimed | mint, owner, lamports | claim_rewards |
| 10 | Shared | mint, from, lamports | share_with_holders |
| 11 | CreatorClaimed | mint, creator, lamports | claim_creator_fees |
| 12 | ProtocolCollected | mint, lamports | collect_protocol_fees |
| 13 | MintCreated | mint, payer, hook program | create_mint |
| 14 | ConfigUpdated | by | init_config, update_config, accept_admin |
HELIX fails with a custom program error from 6000 up. explainFailure(logs) turns a failed transaction's logs into the name and a sentence; errorName(code) gives the name alone.
All 46 errors
| Code | Name | Meaning |
|---|---|---|
| 6000 | InvalidInstruction | The instruction data or accounts are malformed. |
| 6001 | InvalidAccountOwner | An account is not owned by HELIX. |
| 6002 | InvalidAccountKind | An account is not the kind of HELIX account expected. |
| 6003 | InvalidAccountData | An account is too small to be a HELIX account. |
| 6004 | InvalidAddress | An account is not at the address it should be. |
| 6005 | MissingSignature | A required signature is missing. |
| 6006 | NotWritable | An account that must be writable is read-only. |
| 6007 | Unauthorized | The signer has no authority here. |
| 6008 | AlreadyInitialized | The account already exists. |
| 6009 | MathOverflow | A number overflowed. |
| 6010 | MintMismatch | A holding belongs to another token. |
| 6011 | OwnerMismatch | A holding belongs to someone else. |
| 6012 | InsufficientFunds | Not enough tokens or SOL. |
| 6013 | HoldingFrozen | The holding is frozen. |
| 6014 | MaxSupplyExceeded | The mint would pass its maximum supply. |
| 6015 | NoAuthority | That authority has been revoked. |
| 6016 | SameHolding | Source and destination are the same account. |
| 6017 | HoldingNotEmpty | The holding still has tokens. |
| 6018 | HookDataNotEmpty | The holding still keeps hook state (rewards owed). |
| 6019 | InvalidName | A name, symbol or link is too long or empty. |
| 6020 | HookProgramMismatch | The hook program passed is not the mint's hook. |
| 6021 | HookSignerMismatch | The hook signer is not HELIX's signer for that hook. |
| 6022 | HookAccountsMissing | The hook's accounts are missing. |
| 6023 | UnsupportedHookReturn | The hook answered something its flags do not allow. |
| 6024 | DeltaTooLarge | The hook's cuts add up to more than the amount. |
| 6025 | InvalidDeltaAccount | A hook cut names an account it may not credit. |
| 6026 | KitOnlyThroughLaunch | The built-in kit is installed only by a launch. |
| 6027 | Paused | This is paused right now (sells and withdrawals never are). |
| 6028 | PoolMismatch | The pool does not belong to this token. |
| 6029 | SlippageExceeded | The price moved past your slippage limit. |
| 6030 | ZeroAmount | The amount is zero. |
| 6031 | InsufficientLiquidity | The pool does not have enough liquidity. |
| 6032 | CurveLocked | The pool is still a bonding curve; liquidity opens at graduation. |
| 6033 | PoolExists | This token already has a pool. |
| 6034 | LaunchMismatch | The launch does not belong to this token. |
| 6035 | RuleOutOfBounds | A launch rule is outside its bounds. |
| 6036 | CreatorLocked | The creator's wallet is locked until its unlock time. |
| 6037 | EarlyBuyerLocked | Tokens bought in the first seconds are locked until their unlock time. |
| 6038 | MaxWalletExceeded | This would put the wallet above the max-wallet cap. |
| 6039 | NothingToClaim | There is nothing to claim. |
| 6040 | ShareTooSmall | A share must be at least 0.001 SOL. |
| 6041 | FeeCollectorMismatch | The fee collector is not the one in the config. |
| 6042 | ConfigOutOfBounds | A config value is outside its ceiling. |
| 6043 | UnsupportedMint | This token has an extension the bridge refuses (it could freeze or seize the vault), or it is already bridged. |
| 6044 | BridgeNotReady | A launch can go out through the bridge once it has graduated. |
| 6045 | BridgeMismatch | The bridge accounts do not belong to this token. |