Skip to content

Security: Sufon/TICK

Security

SECURITY.md

Security Policy

Reporting a vulnerability

Do not open a public issue for a security report.

Report privately through GitHub's Security Advisories on this repository (Security → Report a vulnerability), which creates a private thread visible only to the maintainer.

Please include: what the issue is, the affected file or instruction, how to reproduce it, and what an attacker gains. A failing test is the most useful thing you can send.

Expect an acknowledgement within 7 days. This is a personal project with no security team behind it, so please plan for a human-scale response time rather than a corporate one.

After launch, embargo stops being the helpful choice

The advice above is written for a program that can still be changed. Once the upgrade authority is revoked, there is no patch, no pause, no admin key and no migration — three instructions, no privileged path, nothing anyone can call to stop it. Read the Scope section before deciding how to handle a finding.

That inverts the usual calculus. Private disclosure normally buys time to ship a fix; here it buys nothing but silence, while anyone else who finds the same thing is free to use it. If you find a live defect in the deployed program:

  • Still tell the maintainer — but treat it as a courtesy heads-up, not an embargo request, and please don't wait on a reply before acting.
  • Assume the honest response is a fast public warning, so holders can act. There is nothing else that can be done for them.
  • A short heads-up is welcome to coordinate wording and timing. Days, not weeks, and never conditional on a fix that cannot exist.

There is deliberately no security.txt compiled into the binary. It is embedded at build time, so adding one would change the artifact hash and invalidate the published reproducible build — for a contact channel this file already provides, serving a disclosure process that cannot end in a fix. This document is the canonical contact point.

Scope

In scope — the on-chain program, programs/tick_distributor. It has three instructions: initialize, fund_reserve, claim.

It has been deployed to Mainnet since 2026-08-12 and its upgrade authority is revoked, so any defect in it is permanent and unpatchable. That window has closed: reports were worth immeasurably more before launch than after, and no report can now change the deployed bytes. They remain worth making — a defect that cannot be patched is one users need to know about, and disclosure is the only remedy left. A rehearsal faucet runs on Devnet under a different program id and is still upgradeable; it is a test fixture, not the launch.

Specifically in scope:

  • Anything that moves TICK out of the vault other than by the published schedule (1 TICK per second, released in completed one-minute batches).
  • Anything that lets a party other than INIT_AUTHORITY call initialize, or that lets initialize be called twice for one mint.
  • Arithmetic that can overflow, truncate, or produce a payout not derivable from the clock.
  • Anything that makes claim fail permanently for everyone, or succeed for a destination the claimant does not own.

In scope — the ceremony scripts under scripts/, insofar as a defect could produce a launch whose on-chain state does not match what was published.

Out of scope:

  • The website and documentation (informational only — no wallet connection, no trading interface, nothing that holds user funds).
  • Advisories in local development tooling that are not reachable from the on-chain program or the launch ceremony. See dependency advisories for the current disposition.
  • Denial of service against a public RPC provider.
  • The economics. A fixed supply distributed for free at 1 TICK/second is the design, not a bug.

Design properties a report can usefully attack

These are the claims the program is built to keep. Breaking any one is a valid finding:

Property Meaning
Fixed supply Total = seconds from genesis to 2100-01-01T00:00:00Z. Minted once.
No mint authority Revoked in the same transaction that creates the supply. Nobody can ever mint again.
No admin path Over an initialized distributor, INIT_AUTHORITY has exactly the power any other address has: none — it cannot mint, pause, withdraw, upgrade, or re-initialize it. It can still initialize a new distributor for a different mint (see below).
No withdraw path Nothing can leave the vault except through claim.
Permissionless claim Anyone may claim; there is no allowlist, no fee, no cooldown, no per-claimant state.
Pure clock math The payout is a function of the chain clock and the distributor's own recorded state.
Immutable The program is finalized at launch. After that, no upgrade path exists.

The one thing INIT_AUTHORITY keeps

Distributors are keyed per mint (seeds = [b"distributor", tick_mint], programs/tick_distributor/src/instructions/initialize.rs). initialize can never run twice for one mint, but INIT_AUTHORITY — and only it — can call initialize for another mint under the same immutable program id, for ever. Such a distributor is fully isolated: separate state, separate vault authority, separate vault, and funds cannot cross between distributors. It cannot mint, move, or affect TICK.

This is not a bug and a report describing it will be closed as known. It is recorded here because it has a real consequence for users: the program id does not identify TICK — the mint address does. Verification must start from the mint.

What this project already did

TICK ships self-audited. No external firm has reviewed this code. There is no pending engagement and none is a precondition of launch — that was decided on 2026-07-27. Weigh everything below accordingly: it is the author auditing the author.

The program was audited internally before launch: property-based tests, mutation testing, three fuzzing campaigns (instruction data, account matrix, state machine), a full ceremony rehearsal on a local validator, and a build verification (scripts/verify-build.sh) that pins the bytecode and IDL hashes. That build reproduces natively: nine builds across eight varied environment dimensions produced one hash. It does not reproduce in the pinned Solana Foundation container 3.1.10 — the container emits a different binary (5d470ad3…; measured 2026-08-11, and independently confirmed by OtterSec's remote verifier), and an earlier version of this document said otherwise. The native build has still never been reproduced independently: every build that matched ran on the author's machine, and no third party has reported the hash. The commands are in "Verifying a deployment yourself" below; running scripts/verify-build.sh and disagreeing with the published value is a finding worth reporting.

That process found real defects — all of them in the off-chain tooling and the operational procedure, none in the on-chain program. That is a statement about where the risk concentrated, not a claim that the program is perfect. An immutable program with three instructions has a small surface; the ceremony that launches it does not.

Verifying a deployment yourself

scripts/verify-build.sh                  # rebuild and compare hashes
scripts/verify-build.sh --rpc <endpoint> # ...and compare against deployed bytes

The --rpc step identifies the cluster by its genesis hash rather than by its hostname, so it works against Mainnet and against any commercial provider; --expect-cluster mainnet-beta makes the identification an assertion. It reads bytes and signs nothing.

The pinned public container needs nothing from this machine except Docker, but it does not reproduce this program — an earlier version of this document said it did, and that was wrong:

solana-verify build --library-name tick_distributor \
  --base-image solanafoundation/solana-verifiable-build:3.1.10

That command yields 5d470ad370c3a08dde93ad61dab3832e424772ebea82bd276bdc2bf3d920f920 — a different binary, not the deployed one. The container's cargo build-sbf and the anchor build that produced the deployed artifact do not emit the same bytes; OtterSec's remote verifier independently produced the same 5d470ad3…. Getting that hash from this command is the known, expected result, not a finding. The rebuild that matches the deployed bytes is scripts/verify-build.sh above — so far demonstrated only on the author's machine.

Pin that base image. Left to infer one from the dependency tree, solana-verify picks 3.0.1, whose Cargo 1.84 cannot parse the edition2024 manifest of block-buffer 0.12.1, and fails with "failed to cargo build" — a stale inference, not a defect in this program. Run it from a clean clone.

Expect two different hashes for the same bytes, and do not report that as a mismatch. The artifact is 214,088 bytes ending in 15 zero bytes. solana-verify strips trailing zeros before hashing and reports f16e47501ce77a61d217792918a19b15c77b7188a8f5f70fdf20629924ffce4c; sha256sum hashes the whole file and reports 568f06ed057a1c29b020cf4f7fcf9aa50b90a1a64edf8f5b9fdeb80e969fdc6d. Both are published in README.md. A native rebuild (scripts/verify-build.sh) that produces neither is the finding.

Every published address is derivable from the mint alone, and the mint — not the program id — is what identifies TICK. You do not have to trust any record in this repository — see docs/operations/DEVNET.md.

Safe harbour

Good-faith research on Devnet or a local validator is welcome and will not be pursued. Do not attack other people's funds, do not test against Mainnet in ways that damage third parties, and give a reasonable window before public disclosure.

There aren't any published security advisories