Skip to content

Commit fdf4ff7

Browse files
committed
fix(token-fundraiser): correct time-window checks and verify vault account
Addresses review feedback: - Flip the contribute/refund day-window comparisons so contributions are accepted during the active window and refunds after it ends. - Record the vault token account in fundraiser state at initialize and verify the caller-supplied vault against it in contribute, check, and refund, preventing a substituted-vault drain.
1 parent 175ed02 commit fdf4ff7

7 files changed

Lines changed: 29 additions & 4 deletions

File tree

tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ pub fn check_contributions(
5454
{
5555
return Err(ProgramError::InvalidAccountData);
5656
}
57+
// The vault must be the fundraiser's recorded vault before we read its
58+
// balance to decide the target has been met.
59+
if &fundraiser_state.vault != vault.address().as_array() {
60+
return Err(ProgramError::InvalidAccountData);
61+
}
5762

5863
// The target amount must have been reached.
5964
let vault_amount = TokenAccount::from_account_view(vault)?.amount();

tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ pub fn contribute(program_id: &Address, accounts: &[AccountView], data: &[u8]) -
5858
if &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() {
5959
return Err(ProgramError::InvalidAccountData);
6060
}
61+
// The vault must be the fundraiser's recorded vault, otherwise a caller
62+
// could record a contribution against an account they control.
63+
if &fundraiser_state.vault != vault.address().as_array() {
64+
return Err(ProgramError::InvalidAccountData);
65+
}
6166

6267
// A contribution must be at least one base unit.
6368
if amount < 1 {
@@ -77,7 +82,7 @@ pub fn contribute(program_id: &Address, accounts: &[AccountView], data: &[u8]) -
7782
// The fundraiser must still be within its active window.
7883
let current_time = Clock::get()?.unix_timestamp;
7984
let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16;
80-
if fundraiser_state.duration > elapsed_days {
85+
if elapsed_days > fundraiser_state.duration {
8186
return Err(FundraiserError::FundraiserEnded.into());
8287
}
8388

tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pub fn initialize(program_id: &Address, accounts: &[AccountView], data: &[u8]) -
106106
time_started: Clock::get()?.unix_timestamp,
107107
duration,
108108
bump,
109+
vault: *vault.address().as_array(),
109110
};
110111
fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?;
111112

tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@ pub fn refund(program_id: &Address, accounts: &[AccountView], _data: &[u8]) -> P
5555
{
5656
return Err(ProgramError::InvalidAccountData);
5757
}
58+
// The vault must be the fundraiser's recorded vault, otherwise refund
59+
// eligibility could be judged from an unrelated token account.
60+
if &fundraiser_state.vault != vault.address().as_array() {
61+
return Err(ProgramError::InvalidAccountData);
62+
}
5863

5964
// The fundraiser must have ended.
6065
let current_time = Clock::get()?.unix_timestamp;
6166
let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16;
62-
if fundraiser_state.duration < elapsed_days {
67+
if elapsed_days < fundraiser_state.duration {
6368
return Err(FundraiserError::FundraiserNotEnded.into());
6469
}
6570

tokens/token-fundraiser/pinocchio/program/src/state.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use pinocchio::error::ProgramError;
1010
/// Serialized byte layout (little-endian), matching the field order below so a
1111
/// Borsh client can deserialize it directly:
1212
/// `[maker: 32][mint_to_raise: 32][amount_to_raise: u64][current_amount: u64]
13-
/// [time_started: i64][duration: u16][bump: u8]`
13+
/// [time_started: i64][duration: u16][bump: u8][vault: 32]`
1414
pub struct Fundraiser {
1515
/// The wallet that created the fundraiser; part of the PDA seeds.
1616
pub maker: [u8; 32],
@@ -26,14 +26,18 @@ pub struct Fundraiser {
2626
pub duration: u16,
2727
/// Canonical bump for the fundraiser PDA.
2828
pub bump: u8,
29+
/// The fundraiser's vault token account (the PDA's associated token account
30+
/// for `mint_to_raise`), recorded at creation. Later instructions check the
31+
/// caller-supplied vault against this to reject a substituted account.
32+
pub vault: [u8; 32],
2933
}
3034

3135
impl Fundraiser {
3236
/// Seed prefix for the fundraiser PDA: `[SEED_PREFIX, maker]`.
3337
pub const SEED_PREFIX: &'static [u8] = b"fundraiser";
3438

3539
/// Serialized size of a `Fundraiser` in bytes.
36-
pub const LEN: usize = 32 + 32 + 8 + 8 + 8 + 2 + 1;
40+
pub const LEN: usize = 32 + 32 + 8 + 8 + 8 + 2 + 1 + 32;
3741

3842
/// Writes the fundraiser into `dst` using the layout documented above.
3943
pub fn serialize(&self, dst: &mut [u8]) -> Result<(), ProgramError> {
@@ -47,6 +51,7 @@ impl Fundraiser {
4751
dst[80..88].copy_from_slice(&self.time_started.to_le_bytes());
4852
dst[88..90].copy_from_slice(&self.duration.to_le_bytes());
4953
dst[90] = self.bump;
54+
dst[91..123].copy_from_slice(&self.vault);
5055
Ok(())
5156
}
5257

@@ -65,6 +70,7 @@ impl Fundraiser {
6570
time_started: i64::from_le_bytes(src[80..88].try_into().unwrap()),
6671
duration: u16::from_le_bytes(src[88..90].try_into().unwrap()),
6772
bump: src[90],
73+
vault: src[91..123].try_into().unwrap(),
6874
})
6975
}
7076
}

tokens/token-fundraiser/pinocchio/tests/account.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const FundraiserSchema = {
88
time_started: "i64",
99
duration: "u16",
1010
bump: "u8",
11+
vault: { array: { type: "u8", len: 32 } },
1112
},
1213
};
1314

@@ -19,6 +20,7 @@ export type FundraiserRaw = {
1920
time_started: bigint;
2021
duration: number;
2122
bump: number;
23+
vault: Uint8Array;
2224
};
2325

2426
// Mirrors the on-chain `Contributor` layout in `program/src/state.rs`.

tokens/token-fundraiser/pinocchio/tests/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe("Fundraiser (Pinocchio)", async () => {
7474
assert(fundraiser.current_amount.toString() === "0", "current amount should start at 0");
7575
assert(fundraiser.duration === values.duration, "wrong duration");
7676
assert(fundraiser.bump === values.fundraiserBump, "wrong bump");
77+
assert(new PublicKey(fundraiser.vault).toBase58() === values.vault.toBase58(), "wrong vault recorded");
7778

7879
const vaultInfo = await client.getAccount(values.vault);
7980
if (vaultInfo === null) throw new Error("Vault account not found");

0 commit comments

Comments
 (0)