Skip to content

Commit 72bb837

Browse files
fix(tokens/hackathon): store name_seed on Hackathon to satisfy IDL builder
Anchor's IDL builder does not support function calls inside seeds = [...] constraints (anchor-syn rejects super::name_seed(&hackathon.name)). Store the SHA-256 hash on the Hackathon account so every other instruction can reference hackathon.name_seed directly. create_hackathon now takes the seed as an extra instruction arg (also an allowed seed expression) and the handler enforces name_seed == sha256(name) to keep the binding.
1 parent 670004b commit 72bb837

10 files changed

Lines changed: 48 additions & 14 deletions

File tree

tokens/hackathon/anchor/programs/hackathon/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ pub enum HackathonError {
2020
PrizeCounterOverflow,
2121
#[msg("Cannot close hackathon: at least one prize is still active")]
2222
PrizesStillActive,
23+
#[msg("name_seed argument does not match sha256(name)")]
24+
NameSeedMismatch,
2325
}

tokens/hackathon/anchor/programs/hackathon/src/instructions/add_prize.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub struct AddPrize<'info> {
2020
#[account(
2121
mut,
2222
has_one = authority,
23-
seeds = [b"hackathon", authority.key().as_ref(), super::name_seed(&hackathon.name).as_ref()],
23+
seeds = [b"hackathon", authority.key().as_ref(), hackathon.name_seed.as_ref()],
2424
bump = hackathon.bump,
2525
)]
2626
pub hackathon: Account<'info, Hackathon>,

tokens/hackathon/anchor/programs/hackathon/src/instructions/cancel_prize.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct CancelPrize<'info> {
2727

2828
#[account(
2929
has_one = authority,
30-
seeds = [b"hackathon", authority.key().as_ref(), super::name_seed(&hackathon.name).as_ref()],
30+
seeds = [b"hackathon", authority.key().as_ref(), hackathon.name_seed.as_ref()],
3131
bump = hackathon.bump,
3232
)]
3333
pub hackathon: Account<'info, Hackathon>,

tokens/hackathon/anchor/programs/hackathon/src/instructions/close_hackathon.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub struct CloseHackathon<'info> {
1919
mut,
2020
has_one = authority,
2121
close = rent_destination,
22-
seeds = [b"hackathon", authority.key().as_ref(), super::name_seed(&hackathon.name).as_ref()],
22+
seeds = [b"hackathon", authority.key().as_ref(), hackathon.name_seed.as_ref()],
2323
bump = hackathon.bump,
2424
)]
2525
pub hackathon: Account<'info, Hackathon>,

tokens/hackathon/anchor/programs/hackathon/src/instructions/create_hackathon.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::state::{Hackathon, HACKATHON_NAME_MAX_LEN};
66
use super::name_seed;
77

88
#[derive(Accounts)]
9-
#[instruction(name: String)]
9+
#[instruction(name: String, name_seed_arg: [u8; 32])]
1010
pub struct CreateHackathon<'info> {
1111
// Pays rent for the Hackathon account. Separate from `authority` so a
1212
// Squads vault PDA (which cannot pay rent directly) can still be the
@@ -21,27 +21,44 @@ pub struct CreateHackathon<'info> {
2121
/// CHECK: stored verbatim as `hackathon.authority`; no onchain reads.
2222
pub authority: UncheckedAccount<'info>,
2323

24+
// The PDA is seeded by `name_seed_arg` (an instruction argument) rather
25+
// than computing `sha256(name)` inside the seed expression — Anchor's
26+
// IDL builder does not support function calls in seed expressions. The
27+
// handler then verifies the binding (`name_seed_arg == sha256(name)`)
28+
// before storing both on the account.
2429
#[account(
2530
init,
2631
payer = payer,
2732
space = Hackathon::DISCRIMINATOR.len() + Hackathon::INIT_SPACE,
28-
seeds = [b"hackathon", authority.key().as_ref(), name_seed(&name).as_ref()],
33+
seeds = [b"hackathon", authority.key().as_ref(), name_seed_arg.as_ref()],
2934
bump
3035
)]
3136
pub hackathon: Account<'info, Hackathon>,
3237

3338
pub system_program: Program<'info, System>,
3439
}
3540

36-
pub fn handle_create_hackathon(context: Context<CreateHackathon>, name: String) -> Result<()> {
41+
pub fn handle_create_hackathon(
42+
context: Context<CreateHackathon>,
43+
name: String,
44+
name_seed_arg: [u8; 32],
45+
) -> Result<()> {
3746
require!(!name.is_empty(), HackathonError::EmptyName);
3847
require!(
3948
name.len() <= HACKATHON_NAME_MAX_LEN,
4049
HackathonError::NameTooLong
4150
);
51+
// Bind `name_seed_arg` to `name` so the seed cannot be chosen
52+
// independently of the stored name. Without this, a caller could create
53+
// a hackathon at any PDA address and claim any human-readable name.
54+
require!(
55+
name_seed_arg == name_seed(&name),
56+
HackathonError::NameSeedMismatch
57+
);
4258

4359
context.accounts.hackathon.set_inner(Hackathon {
4460
authority: context.accounts.authority.key(),
61+
name_seed: name_seed_arg,
4562
prize_count: 0,
4663
bump: context.bumps.hackathon,
4764
name,

tokens/hackathon/anchor/programs/hackathon/src/instructions/pay_winner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub struct PayWinner<'info> {
1616
pub caller: Signer<'info>,
1717

1818
#[account(
19-
seeds = [b"hackathon", hackathon.authority.as_ref(), super::name_seed(&hackathon.name).as_ref()],
19+
seeds = [b"hackathon", hackathon.authority.as_ref(), hackathon.name_seed.as_ref()],
2020
bump = hackathon.bump,
2121
)]
2222
pub hackathon: Account<'info, Hackathon>,

tokens/hackathon/anchor/programs/hackathon/src/instructions/set_winner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub struct SetWinner<'info> {
1111

1212
#[account(
1313
has_one = authority,
14-
seeds = [b"hackathon", authority.key().as_ref(), super::name_seed(&hackathon.name).as_ref()],
14+
seeds = [b"hackathon", authority.key().as_ref(), hackathon.name_seed.as_ref()],
1515
bump = hackathon.bump,
1616
)]
1717
pub hackathon: Account<'info, Hackathon>,

tokens/hackathon/anchor/programs/hackathon/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,16 @@ pub mod hackathon {
2828
// Create a hackathon controlled by `authority` (in practice a Squads vault
2929
// PDA). The hackathon's name is hashed into the PDA seeds so the same
3030
// authority can run multiple hackathons.
31-
pub fn create_hackathon(context: Context<CreateHackathon>, name: String) -> Result<()> {
32-
instructions::create_hackathon::handle_create_hackathon(context, name)
31+
//
32+
// `name_seed` must equal `sha256(name)` — passed as a separate argument
33+
// because Anchor's IDL builder does not support function calls inside
34+
// `seeds = [...]` constraints. The handler enforces the binding.
35+
pub fn create_hackathon(
36+
context: Context<CreateHackathon>,
37+
name: String,
38+
name_seed: [u8; 32],
39+
) -> Result<()> {
40+
instructions::create_hackathon::handle_create_hackathon(context, name, name_seed)
3341
}
3442

3543
// Register a new prize under an existing hackathon. The mint and target

tokens/hackathon/anchor/programs/hackathon/src/state/hackathon.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ pub struct Hackathon {
1212
// program treats it as an opaque pubkey: privileged handlers check
1313
// `signer == authority` and nothing more.
1414
pub authority: Pubkey,
15+
// SHA-256 of `name`, stored on the account so other instructions can
16+
// reference it directly in their `seeds = [...]` constraints without
17+
// calling a function on `name` (Anchor's IDL builder does not support
18+
// function calls in seed expressions). Bound to `name` by the
19+
// `create_hackathon` handler.
20+
pub name_seed: [u8; 32],
1521
// Monotonic counter used to seed Prize PDAs. u8 caps at 255 prizes per
1622
// hackathon, which is plenty for the target use case (30-100 prizes).
1723
pub prize_count: u8,
1824
pub bump: u8,
19-
// Free-form human-readable name. Hashed into the Hackathon PDA seeds so a
20-
// single authority can run multiple hackathons.
25+
// Free-form human-readable name. Bound into the Hackathon PDA via
26+
// `name_seed` so a single authority can run multiple hackathons.
2127
#[max_len(HACKATHON_NAME_MAX_LEN)]
2228
pub name: String,
2329
}

tokens/hackathon/anchor/programs/hackathon/tests/common/world.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ pub fn create_hackathon_instruction(
369369
hackathon: &Pubkey,
370370
name: String,
371371
) -> Instruction {
372+
let name_seed = hackathon_name_seed(&name);
372373
Instruction {
373374
program_id: hackathon::id(),
374375
accounts: hackathon::accounts::CreateHackathon {
@@ -378,7 +379,7 @@ pub fn create_hackathon_instruction(
378379
system_program: anchor_lang::solana_program::system_program::ID,
379380
}
380381
.to_account_metas(None),
381-
data: hackathon::instruction::CreateHackathon { name }.data(),
382+
data: hackathon::instruction::CreateHackathon { name, name_seed }.data(),
382383
}
383384
}
384385

@@ -529,7 +530,7 @@ pub fn prize_vault_address(prize: &Pubkey, mint: &Pubkey) -> Pubkey {
529530

530531
// Local mirror of the program's `name_seed` so tests can derive the PDA
531532
// without depending on the program's private `instructions::shared` module.
532-
fn hackathon_name_seed(name: &str) -> [u8; 32] {
533+
pub fn hackathon_name_seed(name: &str) -> [u8; 32] {
533534
use sha2::{Digest, Sha256};
534535
let mut hasher = Sha256::new();
535536
hasher.update(name.as_bytes());

0 commit comments

Comments
 (0)