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.
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.
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_AUTHORITYcallinitialize, or that letsinitializebe called twice for one mint. - Arithmetic that can overflow, truncate, or produce a payout not derivable from the clock.
- Anything that makes
claimfail 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.
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. |
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.
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.
scripts/verify-build.sh # rebuild and compare hashes
scripts/verify-build.sh --rpc <endpoint> # ...and compare against deployed bytesThe --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.10That 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.
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.