This document describes the security model, trust assumptions, and known limitations of the Torix.fun smart contract (v1).
The protocol recognizes three privileged authorities, all set in GlobalConfig:
| Authority | Risk Level | Mitigation |
|---|---|---|
protocol_authority |
High | Can change fees, round duration, winner count, and all authority addresses. Should be a multisig or governance program. |
end_round_authority |
Medium | Can decide which curves "win" and how much each receives. The backend computes rankings off-chain; the contract only validates payouts. Should be a backend service key with limited scope. |
migration_authority |
Low (currently) | Migration instructions are unimplemented in v1. No risk until v2 is deployed. |
launch, buy_exact, sell_exact, and start_round are permissionless. This is by design:
- Anyone can create a curve.
- Anyone can start a new round (but there is only one active round PDA at a time).
- Anyone can trade.
The RoundVault only receives SOL via round_fee deductions on buy_exact and sell_exact. There are no direct-deposit instructions.
end_round enforces:
now >= round.end_timestamp— round must actually be over.amounts.len() == winners_per_round— exactly the configured number of payouts.- Each
amount > 0— no zero-reward winners. vault_balance - rent >= sum(amounts)— vault is solvent for the proposed payouts.winner.is_writable— winner accounts can receive lamports.curve.owner == torix_program::ID— curves are real program accounts.curve_state.round == current_round— curves belong to the round being closed.curve_state.creator == winner— payout goes to the creator, not an arbitrary address.
After payouts, the vault and round accounts are closed and their rent-exempt lamports sent to fee_recipient.
All arithmetic uses checked operations:
checked_add,checked_sub,checked_mul,checked_divu128intermediates for reserve multiplications (virtual_sol * virtual_tokens)u64::try_fromfor narrowingu128→u64with explicit error mapping
No as u64 casts are used in math paths.
In sell_exact, the curve's spendable balance is computed as:
let curve_rent_exemption = Rent::get()?.minimum_balance(discriminator + CurveState::INIT_SPACE);
let curve_lamports = curve.get_lamports().saturating_sub(curve_rent_exemption);This prevents sells from draining the rent-exempt minimum, ensuring the CurveState account remains valid.
Both trade instructions accept a slippage parameter:
buy_exact(sol_in, min_tokens_out)— fails iftokens_out < min_tokens_outsell_exact(tokens_in, min_sol_out)— fails ifnet_sol_out < min_sol_out
Clients should set these based on current on-chain state with an acceptable tolerance.
fee_recipientaddress is constrained toglobal_config.fee_recipientin trade instructions.end_roundsigner is constrained toglobal_config.end_round_authority.update_globalsigner is constrained toglobal_config.protocol_authority.
All PDAs use Anchor seeds + bump constraints:
CurveState:["curve", mint]RoundState:["round"]RoundVault:["round_vault", round]GlobalConfig:["global_config"]
end_round explicitly verifies that each curve account's owner is the Torix program ID before deserializing its state.
Only one round can exist at a time (["round"]). If a round is started while another is active, the new transaction will fail because the PDA already exists. This is intentional for v1 but limits concurrent rounds.
The contract does not compute rankings on-chain. The backend must:
- Index all curves and their
volume_sol. - Sort by volume.
- Call
end_roundwith the correct winner accounts and amounts.
A compromised or buggy backend could select incorrect winners or amounts. The contract validates that amounts sum to ≤ vault balance and that creators match, but it does not validate that the selected curves are actually the top-N by volume.
start_migration and migrate_liquidity return ErrorCode::NotImplemented. These will be implemented in v2. Until then, no curve can reach Migrated status.
There is no global pause mechanism. If a critical bug is found, the only recourse is to upgrade the program (which requires the upgrade authority) or set extreme fees via update_global.
protocol_authority can set:
fee_bpsup to10_000(100%).round_fee_bpsup tofee_bps.
There is no hard cap below 100%. A malicious or compromised protocol_authority could set fees to 100%, making all trades lose their entire input to fees.
Recommendation: Use a multisig or DAO governance for protocol_authority.