diff --git a/lee/privacy_preserving_circuit/src/execution_state.rs b/lee/privacy_preserving_circuit/src/execution_state.rs index 5ef7a8eb7..99e79de0f 100644 --- a/lee/privacy_preserving_circuit/src/execution_state.rs +++ b/lee/privacy_preserving_circuit/src/execution_state.rs @@ -306,21 +306,16 @@ impl ExecutionState { ) }); - let is_authorized = resolve_authorization_and_record_bindings( + assert_authorization_and_record_bindings( &mut self.pda_family_binding, &mut self.private_pda_bound_positions, &self.private_pda_by_position, &self.globally_authorized, - &caller.authorized_accounts, + &caller, + caller_pda_seeds, pre_account_id, pre_state_position, - caller.program_id, - caller_pda_seeds, - ); - - assert_eq!( - pre_is_authorized, is_authorized, - "Inconsistent authorization for account {pre_account_id}", + pre_is_authorized, ); } Entry::Vacant(_) => { @@ -356,10 +351,6 @@ impl ExecutionState { // Subsequent calls need no re-check because the entry is already recorded on // private_pda_bound_positions. if let Some((authority_program_id, seed)) = external_seed { - assert!( - !pre.is_authorized, - "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" - ); bind_private_pda_position( &mut self.private_pda_bound_positions, pre_state_position, @@ -373,13 +364,25 @@ impl ExecutionState { pre_account_id, ); } + let is_private_pda = self + .private_pda_by_position + .contains_key(&pre_state_position); + if is_private_pda { + assert_authorization_and_record_bindings( + &mut self.pda_family_binding, + &mut self.private_pda_bound_positions, + &self.private_pda_by_position, + &self.globally_authorized, + &caller, + caller_pda_seeds, + pre_account_id, + pre_state_position, + pre_is_authorized, + ); + } // If an account is regular and authorized, make it globally-authorized. - if pre_is_authorized - && !self - .private_pda_by_position - .contains_key(&pre_state_position) - { + if pre_is_authorized && !is_private_pda { self.globally_authorized.insert(pre_account_id); } self.pre_states.push(pre); @@ -563,58 +566,60 @@ fn bind_private_pda_position( } } -/// Resolve the authorization state of a `pre_state` seen again in a chained call and record -/// any resulting bindings. When a caller seed matches, also records the -/// `(caller, seed) → account_id` family binding and, for the private form, marks the position -/// in `private_pda_bound_positions`. Only reachable when `caller_program_id.is_some()`, -/// top-level flows have no caller-emitted seeds, so binding at top level must come from the -/// claim path. Free function so callers can pass individual `&mut self.*` field borrows -/// without holding a borrow on the surrounding struct's other fields. +/// When a caller seed matches, also records the `(caller, seed) → account_id` family binding +/// and, for the private form, marks the position in `private_pda_bound_positions`. Free +/// function so callers can pass individual `&mut self.*` field borrows without holding a borrow +/// on the surrounding struct's other fields. #[expect( clippy::too_many_arguments, reason = "breaking out a context struct does not buy us anything here" )] -fn resolve_authorization_and_record_bindings( +fn assert_authorization_and_record_bindings( pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, private_pda_bound_positions: &mut HashMap, private_pda_by_position: &HashMap, globally_authorized: &HashSet, - caller_authorized: &HashSet, + caller: &CallerData, + caller_pda_seeds: &[PdaSeed], pre_account_id: AccountId, pre_state_position: usize, - caller_program_id: Option, - caller_pda_seeds: &[PdaSeed], -) -> bool { + pre_is_authorized: bool, +) { let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> = - caller_program_id.and_then(|caller| { + caller.program_id.and_then(|caller_program_id| { caller_pda_seeds.iter().find_map(|seed| { - if AccountId::for_public_pda(&caller, seed) == pre_account_id { - return Some((*seed, false, caller)); + if AccountId::for_public_pda(&caller_program_id, seed) == pre_account_id { + return Some((*seed, false, caller_program_id)); } if let Some((npk, vpk, identifier)) = private_pda_by_position.get(&pre_state_position) - && AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier) + && AccountId::for_private_pda(&caller_program_id, seed, npk, vpk, *identifier) == pre_account_id { - return Some((*seed, true, caller)); + return Some((*seed, true, caller_program_id)); } None }) }); - if let Some((seed, is_private_form, caller)) = matched_caller_seed { - assert_family_binding(pda_family_binding, caller, seed, pre_account_id); + if let Some((seed, is_private_form, caller_program_id)) = matched_caller_seed { + assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id); if is_private_form { bind_private_pda_position( private_pda_bound_positions, pre_state_position, - caller, + caller_program_id, seed, ); } } - matched_caller_seed.is_some() + let is_authorized = matched_caller_seed.is_some() || globally_authorized.contains(&pre_account_id) - || caller_authorized.contains(&pre_account_id) + || caller.authorized_accounts.contains(&pre_account_id); + + assert_eq!( + pre_is_authorized, is_authorized, + "Inconsistent authorization for account {pre_account_id}", + ); } diff --git a/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index a01deef2b..aa6e4dae5 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -66,46 +66,28 @@ pub fn compute_circuit_output( WitnessKind::Pda { .. } => pre_state.account_id, }; - match (kind, nullifier) { - ( - WitnessKind::Regular { ask }, - NullifierWitness::Init { .. } | NullifierWitness::Update { .. }, - ) => { - if let Some(ask) = ask { - let derived = NullifierSecretKey::from(ask); - match nullifier { - // Check that the authorization key is actually bound to the - // account Id. - NullifierWitness::Update { nsk, .. } => assert_eq!( - derived, *nsk, - "Authorization secret key does not derive this account's nullifier secret key" - ), - NullifierWitness::Init { npk, .. } => assert_eq!( - NullifierPublicKey::from(&derived), - *npk, - "Authorization secret key does not derive this account's nullifier public key" - ), - } + if let WitnessKind::Regular { ask } = kind { + if let Some(ask) = ask { + let derived = NullifierSecretKey::from(ask); + match nullifier { + // Check that the authorization key is actually bound to the + // account Id. + NullifierWitness::Update { nsk, .. } => assert_eq!( + derived, *nsk, + "Authorization secret key does not derive this account's nullifier secret key" + ), + NullifierWitness::Init { npk, .. } => assert_eq!( + NullifierPublicKey::from(&derived), + *npk, + "Authorization secret key does not derive this account's nullifier public key" + ), } - assert_eq!( - pre_state.is_authorized, - ask.is_some(), - "Regular private account authorization must match the supplied credential" - ); } - (WitnessKind::Pda { .. }, NullifierWitness::Init { .. }) => assert!( - !pre_state.is_authorized, - "Private PDA init requires unauthorized pre_state" - ), - // With an external seed the binding comes from the circuit input and the - // pre_state is intentionally unauthorized; without one the binding comes from - // a Claim or caller pda_seeds, so the pre_state must already be authorized. - // When `binding` is `Some`, execution_state already asserted - // `!pre_state.is_authorized`. - (WitnessKind::Pda { binding }, NullifierWitness::Update { .. }) => assert!( - pre_state.is_authorized ^ binding.is_some(), - "Private PDA update requires authorized pre_state or external seed" - ), + assert_eq!( + pre_state.is_authorized, + ask.is_some(), + "Regular private account authorization must match the supplied credential" + ); } let (new_nullifier, new_nonce, view_tag) = match nullifier { diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index e30162902..3af02b7b6 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -54,15 +54,12 @@ pub enum WitnessKind { /// Private PDA. The npk-to-account_id binding is proven upstream via `Claim::Pda(seed)` or a /// caller's `pda_seeds` match. The identifier diversifies the PDA within the /// `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it as the 4th input. - /// An init is unauthorized; on an update, authorization may be established upstream by a - /// caller `pda_seeds` match or a previously-seen authorization in a chained call. Pda { /// When `Some((authority_program_id, seed))`, the circuit binds this position via the /// external derivation check /// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) == /// pre_state.account_id` rather than requiring a `Claim::Pda` or caller - /// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized - /// == false`. + /// `pda_seeds` to establish the binding. binding: Option<(ProgramId, PdaSeed)>, }, } diff --git a/lee/state_machine/src/lib.rs b/lee/state_machine/src/lib.rs index 4c69c12d5..eceb8f28e 100644 --- a/lee/state_machine/src/lib.rs +++ b/lee/state_machine/src/lib.rs @@ -143,6 +143,14 @@ mod test_methods { ) } + #[must_use] + pub const fn undeclaring_pda_delegator() -> Program { + Program::new_unchecked( + test_methods::UNDECLARING_PDA_DELEGATOR_ID, + Cow::Borrowed(test_methods::UNDECLARING_PDA_DELEGATOR_ELF), + ) + } + #[must_use] pub const fn non_delegating_forwarder() -> Program { Program::new_unchecked( diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs index 299ebcd87..55e052709 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs @@ -974,7 +974,7 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { let mut commitment_set = CommitmentSet::with_capacity(1); commitment_set.extend(std::slice::from_ref(&pda_commitment)); - let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let pda_pre = AccountWithMetadata::new(pda_account, false, pda_id); let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); let program_with_deps = ProgramWithDependencies::new( @@ -1013,6 +1013,54 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { ); } +#[test] +fn private_pda_update_at_root_call_may_not_declare_authorization() { + let program = crate::test_methods::pda_spend_proxy(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 99; + let simple_transfer_id = simple_transfer.id(); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); + let pda_account = Account { + program_owner: simple_transfer_id, + balance: 1, + ..Account::default() + }; + let pda_commitment = Commitment::new(&pda_id, &pda_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&pda_commitment)); + + let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let program_with_deps = + ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into()); + + let result = execute_and_prove( + vec![pda_pre, recipient_pre], + Program::serialize_instruction((seed, 1_u128, simple_transfer_id)).unwrap(), + vec![ + InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk(), + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + }, + }), + InputAccountIdentity::Public, + ], + &program_with_deps, + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + #[test] fn private_pda_init_identifier_mismatch_fails() { let program = crate::test_methods::pda_claimer(); @@ -1020,7 +1068,7 @@ fn private_pda_init_identifier_mismatch_fails() { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); - let pre_state = AccountWithMetadata::new(Account::default(), true, account_id); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let result = execute_and_prove( vec![pre_state], @@ -1041,6 +1089,36 @@ fn private_pda_init_identifier_mismatch_fails() { assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); } +#[test] +fn private_pda_init_at_root_call_may_not_declare_authorization() { + let program = crate::test_methods::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 5; + let account_id = + AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); + let pre_state = AccountWithMetadata::new(Account::default(), true, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + #[test] fn private_pda_update_identifier_mismatch_fails() { let program = crate::test_methods::pda_spend_proxy(); @@ -1059,7 +1137,7 @@ fn private_pda_update_identifier_mismatch_fails() { let mut commitment_set = CommitmentSet::with_capacity(1); commitment_set.extend(std::slice::from_ref(&pda_commitment)); - let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let pda_pre = AccountWithMetadata::new(pda_account, false, pda_id); let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); let program_with_deps = diff --git a/lee/state_machine/src/state/tests/circuit.rs b/lee/state_machine/src/state/tests/circuit.rs index ccbc705ed..1610cceb7 100644 --- a/lee/state_machine/src/state/tests/circuit.rs +++ b/lee/state_machine/src/state/tests/circuit.rs @@ -815,6 +815,64 @@ fn inherited_scope_passes_through_intermediate_calls() { ); } +fn undeclaring_delegation(delegated: bool, external_binding: bool) -> Result<(), LeeError> { + let delegator = crate::test_methods::undeclaring_pda_delegator(); + let callee = crate::test_methods::auth_asserting_noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([77; 32]); + + let delegator_id = delegator.id(); + let account_id = AccountId::for_private_pda(&delegator_id, &seed, &npk, &keys.vpk(), 0); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let callee_id = callee.id(); + let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); + + execute_and_prove( + vec![pre_state], + Program::serialize_instruction(( + delegated.then_some(seed), + callee_id, + Program::serialize_instruction(()).unwrap(), + )) + .unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Pda { + binding: external_binding.then_some((delegator_id, seed)), + }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], + &program_with_deps, + ) + .map(|_| ()) +} + +#[test] +fn delegated_private_pda_first_seen_in_callee_is_authorized() { + undeclaring_delegation(true, true) + .expect("a caller's pda_seeds must authorize a private PDA it delegates at first sight"); +} + +#[test] +fn caller_seeds_bind_a_private_pda_first_seen_in_the_callee() { + undeclaring_delegation(true, false) + .expect("a caller's pda_seeds must bind a private PDA it delegates at first sight"); +} + +#[test] +fn undelegated_private_pda_in_a_callee_may_not_declare_authorization() { + let result = undeclaring_delegation(false, true); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + /// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of /// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide /// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and @@ -891,15 +949,13 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() { let npk = keys.npk(); let seed = PdaSeed::new([99; 32]); - // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = - // true, account_id derived via the private formula. let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); let owned_pre_state = AccountWithMetadata::new( Account { program_owner: program.id(), ..Account::default() }, - true, + false, account_id, ); @@ -1354,7 +1410,7 @@ fn two_private_pda_family_members_receive_and_spend() { let recipient_account = state.get_account_by_id(recipient_id); let (output, proof) = execute_and_prove( vec![ - AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), + AccountWithMetadata::new(alice_pda_0_account, false, alice_pda_0_id), AccountWithMetadata::new(recipient_account, true, recipient_id), ], Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), @@ -1393,7 +1449,7 @@ fn two_private_pda_family_members_receive_and_spend() { let recipient_account = state.get_account_by_id(recipient_id); let (output, proof) = execute_and_prove( vec![ - AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id), + AccountWithMetadata::new(alice_pda_1_account.clone(), false, alice_pda_1_id), AccountWithMetadata::new(recipient_account, false, recipient_id), ], Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), diff --git a/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs b/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs index d8b9bb5ce..86ef73b4a 100644 --- a/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs +++ b/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs @@ -5,7 +5,7 @@ use risc0_zkvm::serde::to_vec; /// Proxy for spending from a private PDA via `simple_transfer`. /// -/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient. +/// `pre_states = [pda, recipient]`. Debits the PDA and credits the recipient. /// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `simple_transfer`. type Instruction = (PdaSeed, u128, ProgramId); @@ -24,15 +24,16 @@ fn main() { return; }; - assert!(first.is_authorized, "first pre_state must be authorized"); - let first_post = AccountPostState::new(first.account.clone()); let second_post = AccountPostState::new(second.account.clone()); + let mut first_for_callee = first.clone(); + first_for_callee.is_authorized = true; + let chained_call = ChainedCall { program_id: simple_transfer_id, instruction_data: to_vec(&amount).unwrap(), - pre_states: vec![first.clone(), second.clone()], + pre_states: vec![first_for_callee, second.clone()], pda_seeds: vec![seed], }; diff --git a/lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs b/lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs index ce7f1d8ee..c6789161e 100644 --- a/lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs +++ b/lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs @@ -51,7 +51,7 @@ fn main() { let recipient_post = AccountPostState::new(recipient_pre.account.clone()); // Chain to simple_transfer with pda_seeds to authorize the PDA. - // The circuit's resolve_authorization_and_record_bindings establishes the + // The circuit's assert_authorization_and_record_bindings establishes the // private PDA (seed, npk) binding when pda_seeds match the private PDA derivation. let mut auth_pda_pre = pda_pre; auth_pda_pre.is_authorized = true; diff --git a/lee/state_machine/test_methods/guest/src/bin/undeclaring_pda_delegator.rs b/lee/state_machine/test_methods/guest/src/bin/undeclaring_pda_delegator.rs new file mode 100644 index 000000000..4b416503c --- /dev/null +++ b/lee/state_machine/test_methods/guest/src/bin/undeclaring_pda_delegator.rs @@ -0,0 +1,38 @@ +use lee_core::program::{ + ChainedCall, InstructionData, PdaSeed, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs, +}; + +type Instruction = (Option, ProgramId, InstructionData); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + mut pre_states, + instruction: (seed, callee_program_id, callee_instruction), + }, + instruction_words, + ) = read_lee_inputs::(); + + let Some(first) = pre_states.first_mut() else { + return; + }; + first.is_authorized = true; + + // Emit an output with only chained calls and no pre or post-states. + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + Vec::new(), + Vec::new(), + ) + .with_chained_calls(vec![ChainedCall { + program_id: callee_program_id, + instruction_data: callee_instruction, + pre_states, + pda_seeds: seed.into_iter().collect(), + }]) + .write(); +} diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index 3cf1cce4e..d38c0897e 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -527,20 +527,20 @@ fn private_key_tree_acc_preparation( let from_identifier = from_acc.kind.identifier(); let from_keys = &from_acc.key_chain; - let ask = from_keys.private_key_holder.authorization_secret_key; + // A PDA is program-authorized and carries no credential of its own. + let ask = (!is_pda).then_some(from_keys.private_key_holder.authorization_secret_key); let nsk = from_keys.private_key_holder.nullifier_secret_key(); let from_npk = from_keys.nullifier_public_key; let from_vpk = from_keys.viewing_public_key.clone(); // TODO: Technically we could allow unauthorized owned accounts, but currently we don't have // support from that in the wallet. - let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), true, account_id); + let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), ask.is_some(), account_id); let random_seed = random_bytes(); Ok(AccountPreparedData { - // A PDA is program-authorized and carries no credential of its own. - ask: (!is_pda).then_some(ask), + ask, nsk: Some(nsk), npk: from_npk, identifier: from_identifier, @@ -593,7 +593,7 @@ fn private_shared_acc_preparation( .map(|e| e.account.clone()) .unwrap_or_default(); - let pre_state = AccountWithMetadata::new(acc, true, account_id); + let pre_state = AccountWithMetadata::new(acc, ask.is_some(), account_id); let random_seed = random_bytes(); diff --git a/test_programs/guest/src/bin/pda_spend_proxy.rs b/test_programs/guest/src/bin/pda_spend_proxy.rs index 5b00004c3..0b4c89145 100644 --- a/test_programs/guest/src/bin/pda_spend_proxy.rs +++ b/test_programs/guest/src/bin/pda_spend_proxy.rs @@ -5,7 +5,7 @@ use risc0_zkvm::serde::to_vec; /// Proxy for spending from a private PDA via `auth_transfer`. /// -/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient. +/// `pre_states = [pda, recipient]`. Debits the PDA and credits the recipient. /// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `auth_transfer`. type Instruction = (PdaSeed, u128, ProgramId); @@ -24,16 +24,17 @@ fn main() { return; }; - assert!(first.is_authorized, "first pre_state must be authorized"); - let first_post = AccountPostState::new(first.account.clone()); let second_post = AccountPostState::new(second.account.clone()); + let mut first_for_callee = first.clone(); + first_for_callee.is_authorized = true; + let chained_call = ChainedCall { program_id: auth_transfer_id, instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) .unwrap(), - pre_states: vec![first.clone(), second.clone()], + pre_states: vec![first_for_callee, second.clone()], pda_seeds: vec![seed], };