Skip to content

Commit 670004b

Browse files
Edward (Edwardbot)mikemaccana-edwardbot
authored andcommitted
test(tokens/hackathon): add Squads 2-of-3 LiteSVM Rust tests
Integration tests build a real Squads v4 multisig (Alice/Bob/Carol, threshold 2-of-3) inside LiteSVM and drive the hackathon program end-to-end through the propose / vote / execute flow. Coverage (9 tests total): - happy path: create_hackathon -> add_prize -> fund -> set_winner (via multisig vote) -> pay_winner (signed by an unrelated bystander wallet). Asserts the winner's token balance equals prize.amount. - pay_winner failure cases: no winner set, vault under-funded, already paid. - set_winner failure case: non-multisig signer rejected by Anchor's has_one = authority constraint. - cancel_prize: drains a funded vault to a refund target, closes the vault, locks the prize against future pay_winner. - close_hackathon: succeeds once every prize is paid or cancelled; fails while any prize is still active. Implementation notes: - Squads onchain program is vendored as a 1.5 MB .so fixture dumped from mainnet (SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf). The README has the refresh command. - The Squads SDK crates (squads-multisig, squads-multisig-program) pull in solana-client 1.17, which conflicts with the Anchor 1.0 / Solana 3.x stack on zeroize. Instead, instruction builders are hand-rolled in tests/common/squads.rs: Anchor 8-byte discriminators (sha256("global: <name>")[..8]), Borsh args, plus the SmallVec<u8, T> wire format used by the compiled-message TransactionMessage struct. - The Squads ProgramConfig account (which would normally be initialised by a Squads admin instruction we cannot run) is forged directly into LiteSVM via set_account with multisig_creation_fee = 0. Also adds the README and bumps Cargo.toml with the sha2 dev-dependency used by the Squads helper module.
1 parent abf6234 commit 670004b

9 files changed

Lines changed: 1709 additions & 0 deletions

File tree

tokens/hackathon/anchor/README.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Anchor Hackathon Prize Program (Squads multisig committee)
2+
3+
A small Anchor 1.0 program for running a hackathon where a Squads multisig
4+
committee controls prize creation and award decisions, but anyone can trigger
5+
the actual onchain payment once a winner is recorded.
6+
7+
## Why it exists
8+
9+
Real hackathon organisers want:
10+
11+
1. **A committee, not a single key.** No one person can quietly mint a
12+
prize, change the winner, or run off with the funds.
13+
2. **Public, auditable awards.** Once the committee has voted "Alice wins
14+
prize #3", anyone can execute the payout — the committee doesn't have to
15+
stay online to hit a button.
16+
3. **Surplus reclaim.** If a prize is funded but never claimed, the
17+
committee can refund it.
18+
19+
This program does the onchain half. Squads handles the offchain voting and
20+
PDA-signing flow.
21+
22+
## How the multisig integration works
23+
24+
The program is multisig-agnostic. Each `Hackathon` account stores a single
25+
`authority: Pubkey`, and every privileged instruction handler checks
26+
`signer == authority`. In practice that pubkey is a Squads vault PDA: the
27+
committee proposes a vault transaction, votes on it, and when the threshold
28+
is reached the Squads program signs the inner instruction with the vault's
29+
PDA. Our program just sees a signed CPI from the vault and proceeds.
30+
31+
This means:
32+
33+
- You can swap Squads for any other multisig (Realms, Mean, a custom one)
34+
without touching this program.
35+
- The program doesn't need to know multisig threshold, member set, or
36+
voting state.
37+
- The program stays under 350 KB of compiled BPF.
38+
39+
## Accounts
40+
41+
```text
42+
Hackathon
43+
authority : Pubkey // Squads vault PDA
44+
name : String // human-readable; hashed into seeds
45+
prize_count : u8 // monotonic counter for Prize PDA seeding
46+
bump : u8
47+
seeds = ["hackathon", authority, sha256(name)]
48+
49+
Prize
50+
hackathon : Pubkey
51+
index : u8 // stable assignment from prize_count
52+
mint : Pubkey // one mint per prize
53+
amount : u64 // exact payout amount
54+
winner : Option<Pubkey>
55+
paid : bool
56+
cancelled : bool
57+
bump : u8
58+
seeds = ["prize", hackathon, index]
59+
60+
Vault = ATA(prize, mint) // Prize PDA owns its own vault
61+
```
62+
63+
Per-prize mints let one hackathon mix denominations (USDC for cash
64+
prizes, governance tokens for runner-up awards). Storing the prize index
65+
in the PDA seed avoids reallocating the `Hackathon` account every time a
66+
prize is added.
67+
68+
## Instruction handlers
69+
70+
| Handler | Signer | Behaviour |
71+
| ------------------ | ---------------- | ---------------------------------------------------------------------- |
72+
| `create_hackathon` | Multisig | Initialise `Hackathon` under `authority`. |
73+
| `add_prize` | Multisig | Register a `Prize` with its own mint and a new vault ATA. |
74+
| `set_winner` | Multisig | Record the winner pubkey for a prize. |
75+
| `pay_winner` | **Anyone** | Transfer exactly `prize.amount` to the winner's token account. |
76+
| `cancel_prize` | Multisig | Drain the vault to a refund target and lock the prize against payout. |
77+
| `close_hackathon` | Multisig | Refund `Hackathon` rent once every prize is paid or cancelled. |
78+
79+
`pay_winner` being permissionless is deliberate. Once the committee has
80+
voted, anyone — the winner, a bot, an organiser's intern — can submit the
81+
transaction. The committee doesn't need to stay online to deliver prizes.
82+
83+
## Token model
84+
85+
SPL Token Interface throughout (`InterfaceAccount<Mint>`,
86+
`InterfaceAccount<TokenAccount>`, `Interface<TokenInterface>`,
87+
`transfer_checked` from `anchor_spl::token_interface`). The same compiled
88+
program works for both classic SPL Token and Token-2022 mints; the choice
89+
is made per prize, at `add_prize` time, by passing the relevant mint.
90+
91+
## Tests
92+
93+
LiteSVM-based Rust integration tests build a real Squads v4 multisig
94+
(Alice / Bob / Carol, threshold 2-of-3) and drive the program end-to-end
95+
through Squads' propose / vote / execute flow.
96+
97+
The Squads onchain program is loaded from a `.so` fixture at
98+
`programs/hackathon/tests/fixtures/squads_multisig.so`. To refresh it from
99+
mainnet:
100+
101+
```
102+
solana program dump --url mainnet-beta \
103+
SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf \
104+
programs/hackathon/tests/fixtures/squads_multisig.so
105+
```
106+
107+
Squads instructions (`multisig_create_v2`, `vault_transaction_create`,
108+
`proposal_create`, `proposal_approve`, `vault_transaction_execute`) are
109+
built by hand in `tests/common/squads.rs`. We don't depend on the
110+
`squads-multisig` SDK crate because it pulls in `solana-client 1.17`,
111+
which conflicts with our Anchor 1.0 / Solana 3.x stack.
112+
113+
The Squads `ProgramConfig` account (normally written by a Squads admin
114+
instruction) is forged directly into LiteSVM with
115+
`multisig_creation_fee = 0`, so test setup is one synchronous call.
116+
117+
### Coverage
118+
119+
- **Happy path**: create → add_prize → fund → set_winner (via multisig
120+
vote) → pay_winner (unpermissioned). Verifies the winner's token
121+
balance equals `prize.amount`.
122+
- **Failure cases**: `pay_winner` rejects when no winner is set, when the
123+
vault is under-funded, and when the prize has already been paid.
124+
`set_winner` rejects a non-multisig signer.
125+
- **Lifecycle**: `cancel_prize` drains a funded vault to a refund target
126+
and locks the prize. `close_hackathon` succeeds once every prize is
127+
resolved and fails while any prize is still active.
128+
129+
## Usage
130+
131+
```
132+
cargo build-sbf
133+
cargo test
134+
```
135+
136+
`cargo build-sbf` must run first because the integration tests load the
137+
compiled `.so` via `include_bytes!`.

tokens/hackathon/anchor/programs/hackathon/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ sha2 = { version = "0.10", default-features = false }
2828
litesvm = "0.11.0"
2929
solana-signer = "3.0.0"
3030
solana-keypair = "3.0.1"
31+
solana-account = "3.0.0"
3132
solana-kite = "0.3.0"
3233
borsh = "1.6.1"
34+
sha2 = "0.10"
3335

3436
[lints.rust]
3537
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Test-side helpers. Split into modules so each file has a single
2+
// responsibility and the test files stay focused on behaviour, not plumbing.
3+
//
4+
// Each integration test compiles `common/` into a separate binary, so an
5+
// item used by one test binary but not another shows up as `dead_code`. The
6+
// allow attribute below silences those false positives across the whole
7+
// helper surface; real dead code is still caught by `cargo clippy`.
8+
#![allow(dead_code)]
9+
10+
pub mod squads;
11+
pub mod world;

0 commit comments

Comments
 (0)