Skip to content

Commit b63b66f

Browse files
committed
perf(program): reduce PDA validation compute units
Replace find_program_address in validate_pda with create_program_address since the bump is already provided in instruction data. Override validate_self in Escrow, Receipt, AllowedMint, and EscrowExtensions to use derive_address with the stored bump, avoiding the off-curve check that is unnecessary for accounts validated at creation. Also documents 2-step admin transfer as a future improvement.
1 parent c5788d1 commit b63b66f

6 files changed

Lines changed: 63 additions & 14 deletions

File tree

docs/IMPROVEMENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ The following enhancements could be considered for future iterations of the prog
1313
5. **Receipt Seed Space Optimization** - The current `receipt_seed` uses a 32-byte `Address` type. Two alternatives could save space:
1414
- **Use `u8` counter**: Change to a simple counter (0-255), saving 31 bytes per receipt. Limits to 256 receipts per depositor/escrow/mint combination, which is acceptable for most use cases.
1515
- **Single receipt with `deposit_additional` instruction**: Allow users to add to an existing receipt rather than creating new ones. This would require handling complexities around `deposited_at` timestamps (e.g., weighted average, use latest, or track per-deposit).
16+
17+
6. **Two-Step Admin Transfer** - The current `UpdateAdmin` instruction requires both the current and new admin to sign the same transaction. This is problematic when transferring to/from multisig wallets (e.g., Squads), since both parties must be present in one transaction. A 2-step pattern (`ProposeAdmin``AcceptAdmin`, with optional `CancelAdminTransfer` and a timeout) would allow async coordination between parties and is the standard pattern for admin handoffs in production programs.

program/src/state/allowed_mint.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@ impl AllowedMint {
6565
mint: &Address,
6666
) -> Result<&'a Self, ProgramError> {
6767
let state = Self::from_bytes(data)?;
68-
let pda = AllowedMintPda::new(escrow, mint);
69-
pda.validate_pda(account, program_id, state.bump)?;
68+
let derived = Address::derive_address(
69+
&[AllowedMintPda::PREFIX, escrow.as_ref(), mint.as_ref()],
70+
Some(state.bump),
71+
program_id,
72+
);
73+
if account.address() != &derived {
74+
return Err(ProgramError::InvalidSeeds);
75+
}
7076
Ok(state)
7177
}
7278
}

program/src/state/escrow.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ impl PdaAccount for Escrow {
7979
fn bump(&self) -> u8 {
8080
self.bump
8181
}
82+
83+
#[inline(always)]
84+
fn validate_self(&self, account: &AccountView, program_id: &Address) -> Result<(), ProgramError> {
85+
let derived = Address::derive_address(&[Self::PREFIX, self.escrow_seed.as_ref()], Some(self.bump), program_id);
86+
if account.address() != &derived {
87+
return Err(ProgramError::InvalidSeeds);
88+
}
89+
Ok(())
90+
}
8291
}
8392

8493
impl Escrow {

program/src/state/escrow_extensions.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,21 +374,26 @@ pub fn update_or_append_extension<const N: usize>(
374374
///
375375
/// Returns `Ok(())` if:
376376
/// - Extensions account is the correct PDA for this escrow
377-
/// - If data exists, the stored bump matches the canonical bump
377+
/// - If data exists, the stored bump matches the derived address
378378
pub fn validate_extensions_pda(escrow: &AccountView, extensions: &AccountView, program_id: &Address) -> ProgramResult {
379-
let extensions_pda = ExtensionsPda::new(escrow.address());
380-
let expected_bump = extensions_pda.validate_pda_address(extensions, program_id)?;
381-
382379
if extensions.data_len() > 0 {
383380
if !extensions.owned_by(program_id) {
384381
return Err(ProgramError::InvalidAccountOwner);
385382
}
386383

387384
let data = extensions.try_borrow()?;
388385
let header = EscrowExtensionsHeader::from_bytes(&data)?;
389-
if header.bump != expected_bump {
386+
387+
// Use stored bump with derive_address (~85 CU) — off-curve already validated at creation
388+
let derived =
389+
Address::derive_address(&[ExtensionsPda::PREFIX, escrow.address().as_ref()], Some(header.bump), program_id);
390+
if extensions.address() != &derived {
390391
return Err(ProgramError::InvalidSeeds);
391392
}
393+
} else {
394+
// Extensions account not yet created — derive canonical bump via find_program_address
395+
let extensions_pda = ExtensionsPda::new(escrow.address());
396+
extensions_pda.validate_pda_address(extensions, program_id)?;
392397
}
393398

394399
Ok(())

program/src/state/receipt.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,25 @@ impl PdaAccount for Receipt {
124124
fn bump(&self) -> u8 {
125125
self.bump
126126
}
127+
128+
#[inline(always)]
129+
fn validate_self(&self, account: &AccountView, program_id: &Address) -> Result<(), ProgramError> {
130+
let derived = Address::derive_address(
131+
&[
132+
Self::PREFIX,
133+
self.escrow.as_ref(),
134+
self.depositor.as_ref(),
135+
self.mint.as_ref(),
136+
self.receipt_seed.as_ref(),
137+
],
138+
Some(self.bump),
139+
program_id,
140+
);
141+
if account.address() != &derived {
142+
return Err(ProgramError::InvalidSeeds);
143+
}
144+
Ok(())
145+
}
127146
}
128147

129148
impl Receipt {

program/src/traits/pda.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,28 @@ pub trait PdaSeeds {
2121
Address::find_program_address(&seeds, program_id)
2222
}
2323

24-
/// Validate that account matches derived PDA
24+
/// Validate that account matches derived PDA using `create_program_address` (~1500 CU).
25+
///
26+
/// Cheaper than `find_program_address` since bump is already known.
27+
/// Use `validate_self` instead when the bump is stored in the account.
2528
#[inline(always)]
26-
fn validate_pda(&self, account: &AccountView, program_id: &Address, expected_bump: u8) -> Result<(), ProgramError> {
27-
let (derived, bump) = self.derive_address(program_id);
28-
if bump != expected_bump {
29-
return Err(ProgramError::InvalidSeeds);
30-
}
29+
fn validate_pda(&self, account: &AccountView, program_id: &Address, bump: u8) -> Result<(), ProgramError> {
30+
let seeds = self.seeds();
31+
let bump_arr = [bump];
32+
let mut seeds_with_bump = seeds;
33+
seeds_with_bump.push(&bump_arr[..]);
34+
let derived =
35+
Address::create_program_address(&seeds_with_bump, program_id).map_err(|_| ProgramError::InvalidSeeds)?;
3136
if account.address() != &derived {
3237
return Err(ProgramError::InvalidSeeds);
3338
}
3439
Ok(())
3540
}
3641

37-
/// Validate that account address matches derived PDA, returns canonical bump
42+
/// Validate that account address matches derived PDA and return the canonical bump.
43+
///
44+
/// Uses `find_program_address` — only call this when the bump is not already known.
45+
/// When bump is available, prefer `validate_pda` or `validate_self`.
3846
#[inline(always)]
3947
fn validate_pda_address(&self, account: &AccountView, program_id: &Address) -> Result<u8, ProgramError> {
4048
let (derived, bump) = self.derive_address(program_id);

0 commit comments

Comments
 (0)