Skip to main content

lichen_core/
processor.rs

1// Lichen Core - Transaction Processor
2
3use crate::account::{Account, Pubkey};
4use crate::consensus::{slot_to_epoch, SLOTS_PER_EPOCH};
5use crate::contract::{
6    build_top_level_call_context, contract_lifecycle_status_for_restriction_mode,
7    derive_contract_lifecycle_from_state_store, ContractAbi, ContractAccount, ContractContext,
8    ContractEvent, ContractRuntime, NativeAccountOp,
9};
10use crate::contract_instruction::ContractInstruction;
11use crate::evm::{
12    decode_evm_transaction, execute_evm_transaction, u256_is_multiple_of_spore, u256_to_spores,
13    EvmReceipt, EvmTxRecord, EVM_PROGRAM_ID,
14};
15use crate::governance::{GovernanceAction, GovernanceProposal};
16use crate::state::{StateBatch, StateStore, SymbolRegistryEntry};
17use crate::transaction::{Instruction, Transaction};
18use crate::{Hash, MAX_CONTRACT_CODE};
19use alloy_primitives::U256;
20use std::collections::HashMap;
21use std::collections::HashSet;
22use std::sync::Mutex;
23
24mod achievement_detection;
25mod batch_access;
26mod contract_execution;
27mod contract_lifecycle;
28mod contract_metadata;
29mod execution;
30mod fees;
31mod governance_authorities;
32mod governance_lifecycle;
33mod governance_oracle;
34mod governance_parsing;
35mod governance_policies;
36mod governed_transfers;
37mod nonce_handlers;
38mod rent_collection;
39mod shielded_handlers;
40mod system_basics;
41mod system_extended;
42mod trust_tier;
43mod validator_lifecycle;
44
45pub use trust_tier::get_trust_tier;
46
47/// Transaction execution result
48#[derive(Debug, Clone)]
49pub struct TxResult {
50    pub success: bool,
51    pub fee_paid: u64,
52    pub error: Option<String>,
53    /// Compute units consumed by this transaction (native + WASM).
54    pub compute_units_used: u64,
55    /// Contract return code (if the transaction includes a contract call).
56    /// This is the raw WASM function return value — interpretation depends on the
57    /// contract's ABI. For LichenID: 0=success, 1=bad input, 2=identity not found, etc.
58    pub return_code: Option<i64>,
59    /// Log messages emitted by the contract during execution.
60    pub contract_logs: Vec<String>,
61    /// Return data set by the contract via `set_return_data()`.
62    pub return_data: Vec<u8>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66enum TxProcessorMode {
67    Canonical,
68    Speculative,
69}
70
71/// Result of deterministic proposal execution against an in-memory batch.
72pub struct SpeculativeBlockExecution {
73    pub results: Vec<TxResult>,
74    pub batch: StateBatch,
75}
76
77/// Persistent transaction execution metadata stored in CF_TX_META.
78/// Extends the old 8-byte CU-only format with full contract result data.
79#[derive(serde::Serialize, serde::Deserialize, Default, Clone, Debug)]
80pub struct TxMeta {
81    pub compute_units_used: u64,
82    pub return_code: Option<i64>,
83    pub return_data: Vec<u8>,
84    pub logs: Vec<String>,
85}
86
87/// Simulation result (dry-run)
88#[derive(Debug, Clone, serde::Serialize)]
89pub struct SimulationResult {
90    pub success: bool,
91    pub fee: u64,
92    pub logs: Vec<String>,
93    pub error: Option<String>,
94    pub compute_used: u64,
95    pub return_data: Option<Vec<u8>>,
96    /// Contract function return code (if a contract call was simulated).
97    pub return_code: Option<i64>,
98    /// Number of storage changes that would be produced by the TX.
99    /// Used by preflight to detect silent failures (success=true, 0 changes).
100    pub state_changes: usize,
101}
102
103#[derive(Debug, Clone)]
104struct SymbolRegistrationSpec {
105    symbol: String,
106    name: Option<String>,
107    template: Option<String>,
108    metadata: Option<serde_json::Value>,
109    decimals: Option<u8>,
110}
111
112const MAX_SYMBOL_REGISTRY_SYMBOL_LEN: usize = 32;
113const MAX_SYMBOL_REGISTRY_NAME_LEN: usize = 128;
114const MAX_SYMBOL_REGISTRY_TEMPLATE_LEN: usize = 32;
115const MAX_SYMBOL_REGISTRY_METADATA_KEY_LEN: usize = 64;
116
117fn validate_symbol_registry_field_length(
118    field: &str,
119    value: &str,
120    max_len: usize,
121) -> Result<(), String> {
122    if value.is_empty() {
123        return Err(format!("RegisterSymbol: '{}' cannot be empty", field));
124    }
125    if value.len() > max_len {
126        return Err(format!(
127            "RegisterSymbol: '{}' exceeds {} bytes",
128            field, max_len
129        ));
130    }
131    Ok(())
132}
133
134#[derive(Debug, Clone, Copy)]
135struct GovernedTransferExecutionPolicy {
136    threshold: u8,
137    execute_after_epoch: u64,
138    velocity_tier: crate::multisig::GovernedTransferVelocityTier,
139    daily_cap_spores: u64,
140}
141
142fn is_evm_instruction(tx: &Transaction) -> bool {
143    tx.message
144        .instructions
145        .first()
146        .map(|ix| ix.program_id == EVM_PROGRAM_ID)
147        .unwrap_or(false)
148}
149
150/// System program ID (all zeros)
151pub const SYSTEM_PROGRAM_ID: Pubkey = Pubkey([0u8; 32]);
152use crate::nft::{
153    decode_collection_state, decode_create_collection_data, decode_mint_nft_data,
154    decode_token_state, encode_collection_state, encode_token_state, CollectionState, TokenState,
155    NFT_COLLECTION_VERSION, NFT_TOKEN_VERSION,
156};
157
158/// Smart contract program ID (all ones)
159pub const CONTRACT_PROGRAM_ID: Pubkey = Pubkey([0xFFu8; 32]);
160
161/// P9-RPC-01: EVM sentinel blockhash — used by `eth_sendRawTransaction` to
162/// mark EVM-wrapped transactions.  The EVM layer provides its own replay
163/// protection via nonces + ECDSA signatures, so native blockhash validation
164/// is skipped for these TXs.  Non-EVM transactions MUST NOT use this hash;
165/// doing so is rejected as an attempted bypass.
166pub const EVM_SENTINEL_BLOCKHASH: Hash = Hash([0xEE; 32]);
167
168/// Slot-based month length (400ms slots, 216,000 per day)
169pub const SLOTS_PER_MONTH: u64 = 216_000 * 30;
170
171/// Free tier: accounts with data ≤ 2KB are exempt from rent
172pub const RENT_FREE_BYTES: u64 = 2048;
173
174/// Number of consecutive missed rent epochs before an account becomes dormant
175pub const DORMANCY_THRESHOLD_EPOCHS: u64 = 2;
176
177const SECONDS_PER_DAY: u64 = 86_400;
178
179/// Maximum age in blocks for a transaction's recent_blockhash.
180/// Transactions referencing a blockhash older than this are rejected.
181pub const MAX_TX_AGE_BLOCKS: u64 = 300;
182/// Base transaction fee (0.001 LICN = 1,000,000 spores)
183/// At $0.10/LICN: $0.0001 per tx  |  At $1.00/LICN: $0.001 per tx
184/// Solana ~$0.00025/tx — Lichen is 2.5x cheaper at $0.10/LICN
185pub const BASE_FEE: u64 = 1_000_000;
186
187/// Contract deployment fee (25 LICN = 25,000,000,000 spores)
188/// At $0.10/LICN: $2.50 per deploy  |  At $1.00/LICN: $25 per deploy
189pub const CONTRACT_DEPLOY_FEE: u64 = 25_000_000_000;
190
191/// Contract upgrade fee (10 LICN = 10,000,000,000 spores)
192/// At $0.10/LICN: $1.00 per upgrade  |  At $1.00/LICN: $10 per upgrade
193pub const CONTRACT_UPGRADE_FEE: u64 = 10_000_000_000;
194
195/// NFT mint fee (0.5 LICN = 500,000,000 spores)
196/// At $0.10/LICN: $0.05 per mint  |  At $1.00/LICN: $0.50 per mint
197pub const NFT_MINT_FEE: u64 = 500_000_000;
198
199/// NFT collection creation fee (1,000 LICN = 1,000,000,000,000 spores)
200/// At $0.10/LICN: $100 per collection  |  At $1.00/LICN: $1,000 per collection
201pub const NFT_COLLECTION_FEE: u64 = 1_000_000_000_000;
202
203/// Minimum balance required to create a nonce account (0.01 LICN = 10,000,000 spores).
204/// Keeps nonce accounts rent-exempt while preventing spam creation.
205pub const NONCE_ACCOUNT_MIN_BALANCE: u64 = 10_000_000;
206
207/// Magic marker stored at data[0] to identify nonce accounts.
208pub const NONCE_ACCOUNT_MARKER: u8 = 0xDA;
209
210// ── Virtual conflict keys for parallel TX scheduling ──
211// These sentinel Pubkeys force transactions that touch the same singleton
212// state (stake pool, MossStake pool, governance counter) into the same
213// scheduling group, preventing lost-update races in parallel execution.
214// Values are chosen to never collide with real versioned Lichen addresses.
215
216/// Virtual key: any TX that reads/writes the stake pool (opcodes 9, 10, 11, 26, 27, 31).
217pub const CONFLICT_KEY_STAKE_POOL: Pubkey = Pubkey([0xFE; 32]);
218/// Virtual key: any TX that reads/writes the MossStake pool (opcodes 13, 14, 15, 16).
219pub const CONFLICT_KEY_MOSSSTAKE_POOL: Pubkey = Pubkey([0xFD; 32]);
220/// Virtual key: any TX that allocates/reads governed proposal IDs (opcode 21).
221pub const CONFLICT_KEY_GOVERNED_PROPOSALS: Pubkey = Pubkey([0xFC; 32]);
222/// Virtual key: any TX that allocates/reads protocol-governance proposal IDs (opcodes 34-37).
223pub const CONFLICT_KEY_GOVERNANCE_PROPOSALS: Pubkey = Pubkey([0xFB; 32]);
224/// Virtual key: any TX that reads/writes native oracle attestation/consensus state (opcode 30).
225pub const CONFLICT_KEY_ORACLE: Pubkey = Pubkey([0xFA; 32]);
226
227pub const GOVERNANCE_ACTION_TREASURY_TRANSFER: u8 = 0;
228pub const GOVERNANCE_ACTION_PARAM_CHANGE: u8 = 1;
229pub const GOVERNANCE_ACTION_CONTRACT_UPGRADE: u8 = 2;
230pub const GOVERNANCE_ACTION_SET_UPGRADE_TIMELOCK: u8 = 3;
231pub const GOVERNANCE_ACTION_EXECUTE_UPGRADE: u8 = 4;
232pub const GOVERNANCE_ACTION_VETO_UPGRADE: u8 = 5;
233pub const GOVERNANCE_ACTION_CONTRACT_CLOSE: u8 = 6;
234pub const GOVERNANCE_ACTION_REGISTER_SYMBOL: u8 = 7;
235pub const GOVERNANCE_ACTION_SET_CONTRACT_ABI: u8 = 8;
236pub const GOVERNANCE_ACTION_CONTRACT_CALL: u8 = 9;
237pub const GOVERNANCE_ACTION_RESTRICT: u8 = 10;
238pub const GOVERNANCE_ACTION_LIFT_RESTRICTION: u8 = 11;
239pub const GOVERNANCE_ACTION_EXTEND_RESTRICTION: u8 = 12;
240
241/// base_fee (spores per transaction)
242pub const GOV_PARAM_BASE_FEE: u8 = 0;
243/// fee_burn_percent (0-100)
244pub const GOV_PARAM_FEE_BURN_PERCENT: u8 = 1;
245/// fee_producer_percent (0-100)
246pub const GOV_PARAM_FEE_PRODUCER_PERCENT: u8 = 2;
247/// fee_voters_percent (0-100)
248pub const GOV_PARAM_FEE_VOTERS_PERCENT: u8 = 3;
249/// fee_treasury_percent (0-100)
250pub const GOV_PARAM_FEE_TREASURY_PERCENT: u8 = 4;
251/// fee_community_percent (0-100)
252pub const GOV_PARAM_FEE_COMMUNITY_PERCENT: u8 = 5;
253/// min_validator_stake (spores)
254pub const GOV_PARAM_MIN_VALIDATOR_STAKE: u8 = 6;
255/// epoch_slots (slots per epoch)
256pub const GOV_PARAM_EPOCH_SLOTS: u8 = 7;
257
258pub const CU_TRANSFER: u64 = 100;
259pub const CU_CREATE_ACCOUNT: u64 = 200;
260pub const CU_CREATE_COLLECTION: u64 = 500;
261pub const CU_MINT_NFT: u64 = 1_000;
262pub const CU_TRANSFER_NFT: u64 = 200;
263pub const CU_STAKE: u64 = 500;
264pub const CU_UNSTAKE: u64 = 500;
265pub const CU_CLAIM_UNSTAKE: u64 = 300;
266pub const CU_REGISTER_EVM: u64 = 200;
267pub const CU_MOSSSTAKE: u64 = 500;
268pub const CU_DEPLOY_CONTRACT: u64 = 5_000;
269pub const CU_SET_CONTRACT_ABI: u64 = 1_000;
270pub const CU_FAUCET_AIRDROP: u64 = 100;
271pub const CU_REGISTER_SYMBOL: u64 = 300;
272pub const CU_GOVERNED_PROPOSAL: u64 = 1_000;
273pub const CU_ZK_SHIELD: u64 = 100_000;
274pub const CU_ZK_TRANSFER: u64 = 200_000;
275pub const CU_REGISTER_VALIDATOR: u64 = 500;
276pub const CU_SLASH_VALIDATOR: u64 = 500;
277pub const CU_NONCE: u64 = 200;
278pub const CU_GOVERNANCE_PARAM: u64 = 300;
279pub const CU_GOVERNANCE_ACTION: u64 = 1_000;
280pub const CU_ORACLE_ATTESTATION: u64 = 500;
281pub const CU_DEREGISTER_VALIDATOR: u64 = 500;
282
283/// Minimum number of assets name bytes (e.g. "BTC" = 3).
284pub const ORACLE_ASSET_MIN_LEN: usize = 1;
285/// Maximum asset name length for oracle attestations.
286pub const ORACLE_ASSET_MAX_LEN: usize = 16;
287/// Oracle attestation staleness window in slots (~1 hour at 400ms/slot).
288pub const ORACLE_STALENESS_SLOTS: u64 = 9_000;
289
290/// Look up the compute-unit cost for a system program instruction by its type byte.
291pub fn compute_units_for_system_ix(instruction_type: u8) -> u64 {
292    match instruction_type {
293        0 | 2..=5 => CU_TRANSFER,
294        1 => CU_CREATE_ACCOUNT,
295        6 => CU_CREATE_COLLECTION,
296        7 => CU_MINT_NFT,
297        8 => CU_TRANSFER_NFT,
298        9 => CU_STAKE,
299        10 => CU_UNSTAKE,
300        11 => CU_CLAIM_UNSTAKE,
301        12 => CU_REGISTER_EVM,
302        13..=16 => CU_MOSSSTAKE,
303        17 => CU_DEPLOY_CONTRACT,
304        18 => CU_SET_CONTRACT_ABI,
305        19 => CU_FAUCET_AIRDROP,
306        20 => CU_REGISTER_SYMBOL,
307        21 | 22 => CU_GOVERNED_PROPOSAL,
308        23 => CU_ZK_SHIELD,
309        24 | 25 => CU_ZK_TRANSFER,
310        26 => CU_REGISTER_VALIDATOR,
311        27 => CU_SLASH_VALIDATOR,
312        28 => CU_NONCE,
313        29 => CU_GOVERNANCE_PARAM,
314        30 => CU_ORACLE_ATTESTATION,
315        31 => CU_DEREGISTER_VALIDATOR,
316        32 | 33 => CU_GOVERNED_PROPOSAL,
317        34..=37 => CU_GOVERNANCE_ACTION,
318        _ => 100,
319    }
320}
321
322/// Compute total compute units for all instructions in a transaction.
323pub fn compute_units_for_tx(tx: &Transaction) -> u64 {
324    let mut total = 0u64;
325    for ix in &tx.message.instructions {
326        if ix.program_id == SYSTEM_PROGRAM_ID {
327            if let Some(&instruction_type) = ix.data.first() {
328                total += compute_units_for_system_ix(instruction_type);
329            }
330        }
331    }
332    total
333}
334
335/// A single validator oracle price attestation.
336#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
337pub struct OracleAttestation {
338    pub validator: Pubkey,
339    pub price: u64,
340    pub decimals: u8,
341    pub stake: u64,
342    pub slot: u64,
343}
344
345/// Consensus oracle price derived from validator attestations.
346#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
347pub struct OracleConsensusPrice {
348    pub asset: String,
349    pub price: u64,
350    pub decimals: u8,
351    pub slot: u64,
352    pub attestation_count: u32,
353}
354
355/// Compute the stake-weighted median price from a set of attestations.
356pub fn compute_stake_weighted_median(attestations: &[OracleAttestation]) -> u64 {
357    if attestations.is_empty() {
358        return 0;
359    }
360    if attestations.len() == 1 {
361        return attestations[0].price;
362    }
363
364    let mut sorted: Vec<(u64, u64)> = attestations.iter().map(|a| (a.price, a.stake)).collect();
365    sorted.sort_by_key(|&(price, _)| price);
366
367    let total_stake: u128 = sorted.iter().map(|&(_, stake)| stake as u128).sum();
368    let half = total_stake / 2;
369
370    let mut cumulative: u128 = 0;
371    for &(price, stake) in &sorted {
372        cumulative += stake as u128;
373        if cumulative > half {
374            return price;
375        }
376    }
377
378    sorted.last().unwrap().0
379}
380
381/// Durable nonce account state — serialized into the account's `data` field.
382#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
383pub struct NonceState {
384    pub authority: Pubkey,
385    pub blockhash: Hash,
386    pub fee_per_signature: u64,
387}
388
389/// Compute graduated rent for an account based on its data size.
390pub fn compute_graduated_rent(data_len: u64, rate_per_kb_per_epoch: u64) -> u64 {
391    if data_len <= RENT_FREE_BYTES {
392        return 0;
393    }
394    let billable = data_len - RENT_FREE_BYTES;
395
396    const TIER1_CAP: u64 = 8 * 1024;
397    const TIER2_CAP: u64 = 98 * 1024;
398
399    let tier1_bytes = billable.min(TIER1_CAP);
400    let tier2_bytes = billable
401        .saturating_sub(TIER1_CAP)
402        .min(TIER2_CAP - TIER1_CAP);
403    let tier3_bytes = billable.saturating_sub(TIER2_CAP);
404
405    let tier1_kb = tier1_bytes.div_ceil(1024);
406    let tier2_kb = tier2_bytes.div_ceil(1024);
407    let tier3_kb = tier3_bytes.div_ceil(1024);
408
409    tier1_kb
410        .saturating_mul(rate_per_kb_per_epoch)
411        .saturating_add(tier2_kb.saturating_mul(rate_per_kb_per_epoch.saturating_mul(2)))
412        .saturating_add(tier3_kb.saturating_mul(rate_per_kb_per_epoch.saturating_mul(4)))
413}
414
415#[derive(Debug, Clone)]
416pub struct FeeConfig {
417    pub base_fee: u64,
418    pub contract_deploy_fee: u64,
419    pub contract_upgrade_fee: u64,
420    pub nft_mint_fee: u64,
421    pub nft_collection_fee: u64,
422    pub fee_burn_percent: u64,
423    pub fee_producer_percent: u64,
424    pub fee_voters_percent: u64,
425    pub fee_treasury_percent: u64,
426    pub fee_community_percent: u64,
427    pub fee_exempt_contracts: Vec<Pubkey>,
428}
429
430impl FeeConfig {
431    pub fn default_from_constants() -> Self {
432        FeeConfig {
433            base_fee: BASE_FEE,
434            contract_deploy_fee: CONTRACT_DEPLOY_FEE,
435            contract_upgrade_fee: CONTRACT_UPGRADE_FEE,
436            nft_mint_fee: NFT_MINT_FEE,
437            nft_collection_fee: NFT_COLLECTION_FEE,
438            fee_burn_percent: 40,
439            fee_producer_percent: 30,
440            fee_voters_percent: 10,
441            fee_treasury_percent: 10,
442            fee_community_percent: 10,
443            fee_exempt_contracts: Vec::new(),
444        }
445    }
446
447    pub fn apply_governance_param(&mut self, param_id: u8, value: u64) -> bool {
448        match param_id {
449            GOV_PARAM_BASE_FEE => {
450                self.base_fee = value;
451                true
452            }
453            GOV_PARAM_FEE_BURN_PERCENT => {
454                self.fee_burn_percent = value;
455                true
456            }
457            GOV_PARAM_FEE_PRODUCER_PERCENT => {
458                self.fee_producer_percent = value;
459                true
460            }
461            GOV_PARAM_FEE_VOTERS_PERCENT => {
462                self.fee_voters_percent = value;
463                true
464            }
465            GOV_PARAM_FEE_TREASURY_PERCENT => {
466                self.fee_treasury_percent = value;
467                true
468            }
469            GOV_PARAM_FEE_COMMUNITY_PERCENT => {
470                self.fee_community_percent = value;
471                true
472            }
473            _ => false,
474        }
475    }
476
477    pub fn validate_distribution(&self) -> Result<(), String> {
478        for (label, value) in [
479            ("burn", self.fee_burn_percent),
480            ("producer", self.fee_producer_percent),
481            ("voters", self.fee_voters_percent),
482            ("treasury", self.fee_treasury_percent),
483            ("community", self.fee_community_percent),
484        ] {
485            if value > 100 {
486                return Err(format!("FeeConfig: {} percentage must be 0..=100", label));
487            }
488        }
489
490        let total = self
491            .fee_burn_percent
492            .saturating_add(self.fee_producer_percent)
493            .saturating_add(self.fee_voters_percent)
494            .saturating_add(self.fee_treasury_percent)
495            .saturating_add(self.fee_community_percent);
496        if total != 100 {
497            return Err(format!(
498                "FeeConfig: fee percentages must sum to 100, got {}",
499                total
500            ));
501        }
502
503        Ok(())
504    }
505}
506
507/// Transaction processor
508pub struct TxProcessor {
509    state: StateStore,
510    batch: Mutex<Option<StateBatch>>,
511    mode: TxProcessorMode,
512    #[allow(clippy::type_complexity)]
513    contract_meta: Mutex<(Option<i64>, Vec<String>, u64, Vec<u8>)>,
514    tx_compute_budget: Mutex<u64>,
515    #[cfg(feature = "zk")]
516    zk_verifier: Mutex<crate::zk::Verifier>,
517}
518
519impl TxProcessor {
520    pub fn new(state: StateStore) -> Self {
521        TxProcessor {
522            state,
523            batch: Mutex::new(None),
524            mode: TxProcessorMode::Canonical,
525            contract_meta: Mutex::new((None, Vec::new(), 0, Vec::new())),
526            tx_compute_budget: Mutex::new(0),
527            #[cfg(feature = "zk")]
528            zk_verifier: Mutex::new(crate::zk::Verifier::new()),
529        }
530    }
531
532    pub fn new_speculative(state: StateStore) -> Self {
533        TxProcessor {
534            state,
535            batch: Mutex::new(None),
536            mode: TxProcessorMode::Speculative,
537            contract_meta: Mutex::new((None, Vec::new(), 0, Vec::new())),
538            tx_compute_budget: Mutex::new(0),
539            #[cfg(feature = "zk")]
540            zk_verifier: Mutex::new(crate::zk::Verifier::new()),
541        }
542    }
543
544    fn is_speculative(&self) -> bool {
545        self.mode == TxProcessorMode::Speculative
546    }
547
548    fn verify_transaction_signatures(tx: &Transaction) -> Result<(), String> {
549        tx.verify_required_signatures().map(|_| ())
550    }
551
552    fn drain_contract_meta(&self) -> (Option<i64>, Vec<String>, u64, Vec<u8>) {
553        let mut meta = self.contract_meta.lock().unwrap_or_else(|e| e.into_inner());
554        (
555            meta.0.take(),
556            std::mem::take(&mut meta.1),
557            std::mem::replace(&mut meta.2, 0),
558            std::mem::take(&mut meta.3),
559        )
560    }
561
562    fn make_result(
563        &self,
564        success: bool,
565        fee_paid: u64,
566        error: Option<String>,
567        compute_units_used: u64,
568    ) -> TxResult {
569        let (return_code, contract_logs, _meta_cu, return_data) = self.drain_contract_meta();
570        TxResult {
571            success,
572            fee_paid,
573            error,
574            compute_units_used,
575            return_code,
576            contract_logs,
577            return_data,
578        }
579    }
580
581    /// Check if a transaction is a valid durable nonce transaction.
582    fn check_durable_nonce(tx: &Transaction, state: &StateStore) -> bool {
583        let first_ix = match tx.message.instructions.first() {
584            Some(ix) => ix,
585            None => return false,
586        };
587
588        if first_ix.program_id != SYSTEM_PROGRAM_ID {
589            return false;
590        }
591        if first_ix.data.len() < 2 || first_ix.data[0] != 28 || first_ix.data[1] != 1 {
592            return false;
593        }
594
595        let nonce_pk = match first_ix.accounts.get(1) {
596            Some(pk) => pk,
597            None => return false,
598        };
599
600        let nonce_account = match state.get_account(nonce_pk) {
601            Ok(Some(account)) => account,
602            _ => return false,
603        };
604
605        match Self::decode_nonce_state(&nonce_account.data) {
606            Ok(nonce_state) => nonce_state.blockhash == tx.message.recent_blockhash,
607            Err(_) => false,
608        }
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use crate::consensus::MIN_VALIDATOR_STAKE;
616    use crate::restrictions::{
617        ProtocolModuleId, RestrictionLiftReason, RestrictionMode, RestrictionReason,
618        RestrictionRecord, RestrictionStatus, RestrictionTarget, GUARDIAN_RESTRICTION_MAX_SLOTS,
619        NATIVE_LICN_ASSET_ID,
620    };
621    use crate::Hash;
622    use crate::Keypair;
623    use tempfile::tempdir;
624
625    /// Helper: set up a processor with treasury, funded alice account, and a genesis block.
626    /// Returns genesis block hash for use as recent_blockhash in test transactions.
627    fn setup() -> (TxProcessor, StateStore, Keypair, Pubkey, Pubkey, Hash) {
628        let temp_dir = tempdir().unwrap();
629        let state = StateStore::open(temp_dir.path()).unwrap();
630        let processor = TxProcessor::new(state.clone());
631
632        let alice_keypair = Keypair::generate();
633        let alice = alice_keypair.pubkey();
634        let treasury = Pubkey([3u8; 32]);
635
636        state.set_treasury_pubkey(&treasury).unwrap();
637        state
638            .put_account(&treasury, &Account::new(0, treasury))
639            .unwrap();
640
641        // Fund alice with 1000 LICN
642        let alice_account = Account::new(1000, alice);
643        state.put_account(&alice, &alice_account).unwrap();
644
645        // Store a genesis block so get_recent_blockhashes returns a real hash
646        let genesis = crate::Block::new_with_timestamp(
647            0,
648            Hash::default(),
649            Hash::default(),
650            [0u8; 32],
651            Vec::new(),
652            0,
653        );
654        let genesis_hash = genesis.hash();
655        state.put_block(&genesis).unwrap();
656        state.set_last_slot(0).unwrap();
657
658        (
659            processor,
660            state,
661            alice_keypair,
662            alice,
663            treasury,
664            genesis_hash,
665        )
666    }
667
668    fn advance_test_slot(state: &StateStore, slot: u64) -> Hash {
669        let parent_hash = state
670            .get_block_by_slot(state.get_last_slot().unwrap())
671            .unwrap()
672            .map(|block| block.hash())
673            .unwrap_or_default();
674        let block = crate::Block::new_with_timestamp(
675            slot,
676            parent_hash,
677            Hash::default(),
678            [0u8; 32],
679            Vec::new(),
680            slot,
681        );
682        let blockhash = block.hash();
683        state.put_block(&block).unwrap();
684        state.set_last_slot(slot).unwrap();
685        blockhash
686    }
687
688    /// Helper: build and sign a transfer tx
689    fn make_transfer_tx(
690        from_kp: &Keypair,
691        from: Pubkey,
692        to: Pubkey,
693        amount_licn: u64,
694        recent_blockhash: Hash,
695    ) -> Transaction {
696        let mut data = vec![0u8];
697        data.extend_from_slice(&Account::licn_to_spores(amount_licn).to_le_bytes());
698
699        let ix = Instruction {
700            program_id: SYSTEM_PROGRAM_ID,
701            accounts: vec![from, to],
702            data,
703        };
704
705        let message = crate::transaction::Message::new(vec![ix], recent_blockhash);
706        let mut tx = Transaction::new(message);
707        let sig = from_kp.sign(&tx.message.serialize());
708        tx.signatures.push(sig);
709        tx
710    }
711
712    #[test]
713    fn test_transfer() {
714        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
715        let bob = Pubkey([2u8; 32]);
716        let validator = Pubkey([42u8; 32]);
717
718        let tx = make_transfer_tx(&alice_kp, alice, bob, 100, genesis_hash);
719        let result = processor.process_transaction(&tx, &validator);
720
721        assert!(result.success);
722        assert_eq!(result.fee_paid, BASE_FEE);
723        assert_eq!(
724            state.get_balance(&bob).unwrap(),
725            Account::licn_to_spores(100)
726        );
727    }
728
729    #[test]
730    fn test_replay_protection_rejects_bad_blockhash() {
731        let (processor, _state, alice_kp, alice, _treasury, _genesis_hash) = setup();
732        let bob = Pubkey([2u8; 32]);
733        let validator = Pubkey([42u8; 32]);
734
735        // Use a random blockhash that's not in recent history
736        let bad_hash = Hash::hash(b"nonexistent_block");
737        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, bad_hash);
738        let result = processor.process_transaction(&tx, &validator);
739
740        assert!(
741            !result.success,
742            "Tx with invalid recent_blockhash should be rejected"
743        );
744        assert!(result.error.unwrap().contains("Blockhash not found"));
745    }
746
747    #[test]
748    fn test_replay_protection_accepts_genesis_hash() {
749        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
750        let bob = Pubkey([2u8; 32]);
751        let validator = Pubkey([42u8; 32]);
752
753        // Real genesis block hash is valid (stored in recent blockhashes)
754        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
755        let result = processor.process_transaction(&tx, &validator);
756
757        assert!(
758            result.success,
759            "Tx with genesis blockhash should be accepted"
760        );
761    }
762
763    #[test]
764    fn test_unsigned_tx_rejected() {
765        let (processor, _state, _alice_kp, alice, _treasury, genesis_hash) = setup();
766        let bob = Pubkey([2u8; 32]);
767        let validator = Pubkey([42u8; 32]);
768
769        // Build tx but DON'T sign it
770        let mut data = vec![0u8];
771        data.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
772        let ix = Instruction {
773            program_id: SYSTEM_PROGRAM_ID,
774            accounts: vec![alice, bob],
775            data,
776        };
777        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
778        let tx = Transaction::new(message);
779
780        let result = processor.process_transaction(&tx, &validator);
781        assert!(!result.success, "Unsigned tx should be rejected");
782    }
783
784    #[test]
785    fn test_wrong_signer_rejected() {
786        let (processor, _state, _alice_kp, alice, _treasury, genesis_hash) = setup();
787        let bob = Pubkey([2u8; 32]);
788        let validator = Pubkey([42u8; 32]);
789
790        // Sign with a DIFFERENT key
791        let eve_kp = Keypair::generate();
792
793        let mut data = vec![0u8];
794        data.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
795        let ix = Instruction {
796            program_id: SYSTEM_PROGRAM_ID,
797            accounts: vec![alice, bob],
798            data,
799        };
800        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
801        let mut tx = Transaction::new(message);
802        let sig = eve_kp.sign(&tx.message.serialize());
803        tx.signatures.push(sig);
804
805        let result = processor.process_transaction(&tx, &validator);
806        assert!(!result.success, "Tx signed by wrong key should be rejected");
807    }
808
809    #[test]
810    fn test_multi_instruction_tx() {
811        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
812        let bob = Pubkey([2u8; 32]);
813        let charlie = Pubkey([4u8; 32]);
814        let validator = Pubkey([42u8; 32]);
815
816        // Two instructions, both from alice
817        let mut data1 = vec![0u8];
818        data1.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
819        let ix1 = Instruction {
820            program_id: SYSTEM_PROGRAM_ID,
821            accounts: vec![alice, bob],
822            data: data1,
823        };
824
825        let mut data2 = vec![0u8];
826        data2.extend_from_slice(&Account::licn_to_spores(20).to_le_bytes());
827        let ix2 = Instruction {
828            program_id: SYSTEM_PROGRAM_ID,
829            accounts: vec![alice, charlie],
830            data: data2,
831        };
832
833        let message = crate::transaction::Message::new(vec![ix1, ix2], genesis_hash);
834        let mut tx = Transaction::new(message);
835        let sig = alice_kp.sign(&tx.message.serialize());
836        tx.signatures.push(sig);
837
838        let result = processor.process_transaction(&tx, &validator);
839        assert!(
840            result.success,
841            "Multi-instruction tx from same signer should work"
842        );
843
844        assert_eq!(
845            state.get_balance(&bob).unwrap(),
846            Account::licn_to_spores(10)
847        );
848        assert_eq!(
849            state.get_balance(&charlie).unwrap(),
850            Account::licn_to_spores(20)
851        );
852    }
853
854    #[test]
855    fn test_fee_deducted_from_payer() {
856        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
857        let bob = Pubkey([2u8; 32]);
858        let validator = Pubkey([42u8; 32]);
859
860        let initial_balance = state.get_balance(&alice).unwrap();
861        let transfer_amount = Account::licn_to_spores(50);
862        let tx = make_transfer_tx(&alice_kp, alice, bob, 50, genesis_hash);
863        let result = processor.process_transaction(&tx, &validator);
864
865        assert!(result.success);
866        let final_balance = state.get_balance(&alice).unwrap();
867        assert_eq!(final_balance, initial_balance - transfer_amount - BASE_FEE);
868    }
869
870    #[test]
871    fn test_insufficient_balance_rejected() {
872        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
873        let bob = Pubkey([2u8; 32]);
874        let validator = Pubkey([42u8; 32]);
875
876        // Alice has 1000 LICN, try to send 2000
877        let tx = make_transfer_tx(&alice_kp, alice, bob, 2000, genesis_hash);
878        let result = processor.process_transaction(&tx, &validator);
879
880        assert!(!result.success, "Oversized transfer should be rejected");
881    }
882
883    // ─── MossStake instruction tests ──────────────────────────────────
884
885    /// Helper: build and sign a MossStake deposit tx (instruction type 13)
886    fn make_mossstake_deposit_tx(
887        kp: &Keypair,
888        user: Pubkey,
889        amount_spores: u64,
890        recent_blockhash: Hash,
891    ) -> Transaction {
892        let mut data = vec![13u8];
893        data.extend_from_slice(&amount_spores.to_le_bytes());
894        let ix = Instruction {
895            program_id: SYSTEM_PROGRAM_ID,
896            accounts: vec![user],
897            data,
898        };
899        let message = crate::transaction::Message::new(vec![ix], recent_blockhash);
900        let mut tx = Transaction::new(message);
901        tx.signatures.push(kp.sign(&tx.message.serialize()));
902        tx
903    }
904
905    /// Helper: build and sign a MossStake unstake tx (instruction type 14)
906    fn make_mossstake_unstake_tx(
907        kp: &Keypair,
908        user: Pubkey,
909        st_licn_amount: u64,
910        recent_blockhash: Hash,
911    ) -> Transaction {
912        let mut data = vec![14u8];
913        data.extend_from_slice(&st_licn_amount.to_le_bytes());
914        let ix = Instruction {
915            program_id: SYSTEM_PROGRAM_ID,
916            accounts: vec![user],
917            data,
918        };
919        let message = crate::transaction::Message::new(vec![ix], recent_blockhash);
920        let mut tx = Transaction::new(message);
921        tx.signatures.push(kp.sign(&tx.message.serialize()));
922        tx
923    }
924
925    /// Helper: build and sign a MossStake claim tx (instruction type 15)
926    fn make_mossstake_claim_tx(kp: &Keypair, user: Pubkey, recent_blockhash: Hash) -> Transaction {
927        let data = vec![15u8];
928        let ix = Instruction {
929            program_id: SYSTEM_PROGRAM_ID,
930            accounts: vec![user],
931            data,
932        };
933        let message = crate::transaction::Message::new(vec![ix], recent_blockhash);
934        let mut tx = Transaction::new(message);
935        tx.signatures.push(kp.sign(&tx.message.serialize()));
936        tx
937    }
938
939    fn make_mossstake_transfer_tx(
940        kp: &Keypair,
941        from: Pubkey,
942        to: Pubkey,
943        st_licn_amount: u64,
944        recent_blockhash: Hash,
945    ) -> Transaction {
946        let mut data = vec![16u8];
947        data.extend_from_slice(&st_licn_amount.to_le_bytes());
948        let ix = Instruction {
949            program_id: SYSTEM_PROGRAM_ID,
950            accounts: vec![from, to],
951            data,
952        };
953        let message = crate::transaction::Message::new(vec![ix], recent_blockhash);
954        let mut tx = Transaction::new(message);
955        tx.signatures.push(kp.sign(&tx.message.serialize()));
956        tx
957    }
958
959    #[test]
960    fn test_mossstake_deposit_reduces_balance() {
961        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
962        let validator = Pubkey([42u8; 32]);
963
964        let deposit_amount = Account::licn_to_spores(100);
965        let initial_balance = state.get_balance(&alice).unwrap();
966
967        let tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
968        let result = processor.process_transaction(&tx, &validator);
969
970        assert!(
971            result.success,
972            "MossStake deposit should succeed: {:?}",
973            result.error
974        );
975
976        let final_balance = state.get_balance(&alice).unwrap();
977        // Balance should decrease by deposit + fee
978        assert_eq!(
979            final_balance,
980            initial_balance - deposit_amount - result.fee_paid
981        );
982
983        // Pool should have the staked amount
984        let pool = state.get_mossstake_pool().unwrap();
985        assert_eq!(pool.st_licn_token.total_licn_staked, deposit_amount);
986        assert!(pool.positions.contains_key(&alice));
987    }
988
989    #[test]
990    fn test_mossstake_deposit_zero_rejected() {
991        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
992        let validator = Pubkey([42u8; 32]);
993
994        let tx = make_mossstake_deposit_tx(&alice_kp, alice, 0, genesis_hash);
995        let result = processor.process_transaction(&tx, &validator);
996
997        assert!(!result.success, "Zero deposit should be rejected");
998    }
999
1000    #[test]
1001    fn test_mossstake_deposit_insufficient_balance() {
1002        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
1003        let validator = Pubkey([42u8; 32]);
1004
1005        // Alice has 1000 LICN, try to deposit 2000
1006        let tx = make_mossstake_deposit_tx(
1007            &alice_kp,
1008            alice,
1009            Account::licn_to_spores(2000),
1010            genesis_hash,
1011        );
1012        let result = processor.process_transaction(&tx, &validator);
1013
1014        assert!(!result.success, "Over-balance deposit should be rejected");
1015    }
1016
1017    #[test]
1018    fn test_mossstake_unstake_creates_request() {
1019        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1020        let validator = Pubkey([42u8; 32]);
1021
1022        // First deposit
1023        let deposit_amount = Account::licn_to_spores(200);
1024        let tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1025        let result = processor.process_transaction(&tx, &validator);
1026        assert!(result.success, "Deposit should succeed");
1027
1028        // Get the stLICN minted (1:1 on first deposit)
1029        let pool = state.get_mossstake_pool().unwrap();
1030        let st_licn = pool.positions.get(&alice).unwrap().st_licn_amount;
1031        assert_eq!(st_licn, deposit_amount);
1032
1033        // Request unstake
1034        let tx = make_mossstake_unstake_tx(&alice_kp, alice, st_licn, genesis_hash);
1035        let result = processor.process_transaction(&tx, &validator);
1036        assert!(result.success, "Unstake should succeed: {:?}", result.error);
1037
1038        // Check pending unstake request exists
1039        let pool = state.get_mossstake_pool().unwrap();
1040        let requests = pool.get_unstake_requests(&alice);
1041        assert_eq!(requests.len(), 1);
1042        assert_eq!(requests[0].licn_to_receive, deposit_amount);
1043    }
1044
1045    #[test]
1046    fn test_mossstake_claim_before_cooldown_fails() {
1047        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1048        let validator = Pubkey([42u8; 32]);
1049
1050        // Deposit then unstake
1051        let deposit_amount = Account::licn_to_spores(100);
1052        let tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1053        assert!(processor.process_transaction(&tx, &validator).success);
1054
1055        let pool = state.get_mossstake_pool().unwrap();
1056        let st_licn = pool.positions.get(&alice).unwrap().st_licn_amount;
1057
1058        let tx = make_mossstake_unstake_tx(&alice_kp, alice, st_licn, genesis_hash);
1059        assert!(processor.process_transaction(&tx, &validator).success);
1060
1061        // Try claim immediately (slot 0, cooldown is 151200 slots)
1062        let tx = make_mossstake_claim_tx(&alice_kp, alice, genesis_hash);
1063        let result = processor.process_transaction(&tx, &validator);
1064        assert!(!result.success, "Claim before cooldown should fail");
1065    }
1066
1067    #[test]
1068    fn test_mossstake_claim_after_cooldown_succeeds() {
1069        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1070        let validator = Pubkey([42u8; 32]);
1071
1072        let initial_balance = state.get_balance(&alice).unwrap();
1073
1074        // Deposit
1075        let deposit_amount = Account::licn_to_spores(100);
1076        let tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1077        let r1 = processor.process_transaction(&tx, &validator);
1078        assert!(r1.success);
1079
1080        // Unstake
1081        let pool = state.get_mossstake_pool().unwrap();
1082        let st_licn = pool.positions.get(&alice).unwrap().st_licn_amount;
1083        let tx = make_mossstake_unstake_tx(&alice_kp, alice, st_licn, genesis_hash);
1084        let r2 = processor.process_transaction(&tx, &validator);
1085        assert!(r2.success);
1086
1087        // Advance the slot beyond cooldown (1,512,000 = 7 days at 400ms/slot)
1088        // Create a new block at a slot past the cooldown period
1089        let future_block = crate::Block::new_with_timestamp(
1090            2_000_000,
1091            genesis_hash,
1092            Hash::hash(b"future_state"),
1093            [0u8; 32],
1094            Vec::new(),
1095            999_999,
1096        );
1097        let future_hash = future_block.hash();
1098        state.put_block(&future_block).unwrap();
1099        state.set_last_slot(2_000_000).unwrap();
1100
1101        // Claim should succeed now
1102        let tx = make_mossstake_claim_tx(&alice_kp, alice, future_hash);
1103        let r3 = processor.process_transaction(&tx, &validator);
1104        assert!(
1105            r3.success,
1106            "Claim after cooldown should succeed: {:?}",
1107            r3.error
1108        );
1109
1110        // Balance should be restored minus all fees
1111        let final_balance = state.get_balance(&alice).unwrap();
1112        let total_fees = r1.fee_paid + r2.fee_paid + r3.fee_paid;
1113        assert_eq!(final_balance, initial_balance - total_fees);
1114    }
1115
1116    #[test]
1117    fn test_mossstake_unstake_more_than_staked_fails() {
1118        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
1119        let validator = Pubkey([42u8; 32]);
1120
1121        // Deposit 100 LICN
1122        let deposit_amount = Account::licn_to_spores(100);
1123        let tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1124        assert!(processor.process_transaction(&tx, &validator).success);
1125
1126        // Try to unstake 200 LICN worth of stLICN
1127        let too_much = Account::licn_to_spores(200);
1128        let tx = make_mossstake_unstake_tx(&alice_kp, alice, too_much, genesis_hash);
1129        let result = processor.process_transaction(&tx, &validator);
1130        assert!(!result.success, "Unstaking more than staked should fail");
1131    }
1132
1133    #[test]
1134    fn test_mossstake_deposit_rejects_outgoing_restricted_depositor() {
1135        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1136        let validator = Pubkey([42u8; 32]);
1137        let before_pool = state.get_mossstake_pool().unwrap();
1138
1139        put_active_processor_test_restriction(
1140            &state,
1141            RestrictionTarget::Account(alice),
1142            RestrictionMode::OutgoingOnly,
1143        );
1144
1145        let tx =
1146            make_mossstake_deposit_tx(&alice_kp, alice, Account::licn_to_spores(100), genesis_hash);
1147        let result = processor.process_transaction(&tx, &validator);
1148        assert!(!result.success);
1149        assert!(result
1150            .error
1151            .as_deref()
1152            .unwrap_or("")
1153            .contains("MossStakeDeposit blocked by active depositor account restriction"));
1154
1155        let after_pool = state.get_mossstake_pool().unwrap();
1156        assert_eq!(after_pool.st_licn_token.total_licn_staked, 0);
1157        assert_eq!(after_pool.positions.len(), before_pool.positions.len());
1158        assert!(!after_pool.positions.contains_key(&alice));
1159    }
1160
1161    #[test]
1162    fn test_mossstake_deposit_rejects_native_frozen_amount() {
1163        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1164        let validator = Pubkey([42u8; 32]);
1165        let authority_spendable = state.get_account(&alice).unwrap().unwrap().spendable;
1166
1167        put_active_processor_test_restriction(
1168            &state,
1169            RestrictionTarget::AccountAsset {
1170                account: alice,
1171                asset: NATIVE_LICN_ASSET_ID,
1172            },
1173            RestrictionMode::FrozenAmount {
1174                amount: authority_spendable,
1175            },
1176        );
1177
1178        let tx =
1179            make_mossstake_deposit_tx(&alice_kp, alice, Account::licn_to_spores(100), genesis_hash);
1180        let result = processor.process_transaction(&tx, &validator);
1181        assert!(!result.success);
1182        assert!(result.error.as_deref().unwrap_or("").contains(
1183            "MossStakeDeposit blocked by active depositor native account-asset restriction"
1184        ));
1185
1186        let pool = state.get_mossstake_pool().unwrap();
1187        assert_eq!(pool.st_licn_token.total_licn_staked, 0);
1188        assert!(!pool.positions.contains_key(&alice));
1189    }
1190
1191    #[test]
1192    fn test_mossstake_protocol_pause_rejects_deposit_without_position() {
1193        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1194        let validator = Pubkey([42u8; 32]);
1195
1196        put_active_processor_test_restriction(
1197            &state,
1198            RestrictionTarget::ProtocolModule(ProtocolModuleId::MossStake),
1199            RestrictionMode::ProtocolPaused,
1200        );
1201
1202        let tx =
1203            make_mossstake_deposit_tx(&alice_kp, alice, Account::licn_to_spores(100), genesis_hash);
1204        let result = processor.process_transaction(&tx, &validator);
1205        assert!(!result.success);
1206        assert!(result
1207            .error
1208            .as_deref()
1209            .unwrap_or("")
1210            .contains("MossStakeDeposit blocked by active MossStake protocol pause"));
1211
1212        let pool = state.get_mossstake_pool().unwrap();
1213        assert_eq!(pool.st_licn_token.total_licn_staked, 0);
1214        assert!(!pool.positions.contains_key(&alice));
1215    }
1216
1217    #[test]
1218    fn test_mossstake_unstake_rejects_outgoing_restricted_position_owner() {
1219        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1220        let validator = Pubkey([42u8; 32]);
1221        let deposit_amount = Account::licn_to_spores(100);
1222        let deposit_tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1223        assert!(
1224            processor
1225                .process_transaction(&deposit_tx, &validator)
1226                .success
1227        );
1228
1229        let before_pool = state.get_mossstake_pool().unwrap();
1230        let st_licn = before_pool.positions.get(&alice).unwrap().st_licn_amount;
1231        put_active_processor_test_restriction(
1232            &state,
1233            RestrictionTarget::Account(alice),
1234            RestrictionMode::OutgoingOnly,
1235        );
1236
1237        let tx = make_mossstake_unstake_tx(&alice_kp, alice, st_licn, genesis_hash);
1238        let result = processor.process_transaction(&tx, &validator);
1239        assert!(!result.success);
1240        assert!(result
1241            .error
1242            .as_deref()
1243            .unwrap_or("")
1244            .contains("MossStakeUnstake blocked by active position owner account restriction"));
1245
1246        let after_pool = state.get_mossstake_pool().unwrap();
1247        assert_eq!(
1248            after_pool.positions.get(&alice).unwrap().st_licn_amount,
1249            st_licn
1250        );
1251        assert!(after_pool.get_unstake_requests(&alice).is_empty());
1252    }
1253
1254    #[test]
1255    fn test_mossstake_claim_rejects_incoming_restricted_user_without_dropping_request() {
1256        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1257        let validator = Pubkey([42u8; 32]);
1258        let deposit_amount = Account::licn_to_spores(100);
1259        let deposit_tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1260        assert!(
1261            processor
1262                .process_transaction(&deposit_tx, &validator)
1263                .success
1264        );
1265
1266        let pool = state.get_mossstake_pool().unwrap();
1267        let st_licn = pool.positions.get(&alice).unwrap().st_licn_amount;
1268        let unstake_tx = make_mossstake_unstake_tx(&alice_kp, alice, st_licn, genesis_hash);
1269        assert!(
1270            processor
1271                .process_transaction(&unstake_tx, &validator)
1272                .success
1273        );
1274        let before_requests = state
1275            .get_mossstake_pool()
1276            .unwrap()
1277            .get_unstake_requests(&alice);
1278        assert_eq!(before_requests.len(), 1);
1279
1280        put_active_processor_test_restriction(
1281            &state,
1282            RestrictionTarget::Account(alice),
1283            RestrictionMode::IncomingOnly,
1284        );
1285        let future_hash = advance_test_slot(&state, crate::consensus::UNSTAKE_COOLDOWN_SLOTS + 1);
1286        let claim_tx = make_mossstake_claim_tx(&alice_kp, alice, future_hash);
1287        let result = processor.process_transaction(&claim_tx, &validator);
1288        assert!(!result.success);
1289        assert!(result
1290            .error
1291            .as_deref()
1292            .unwrap_or("")
1293            .contains("MossStakeClaim blocked by active user account restriction"));
1294
1295        let after_requests = state
1296            .get_mossstake_pool()
1297            .unwrap()
1298            .get_unstake_requests(&alice);
1299        assert_eq!(after_requests.len(), before_requests.len());
1300        assert_eq!(
1301            after_requests[0].licn_to_receive,
1302            before_requests[0].licn_to_receive
1303        );
1304    }
1305
1306    #[test]
1307    fn test_mossstake_transfer_rejects_outgoing_restricted_sender() {
1308        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1309        let validator = Pubkey([42u8; 32]);
1310        let bob = Pubkey([0xB2; 32]);
1311        let deposit_amount = Account::licn_to_spores(100);
1312        let deposit_tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1313        assert!(
1314            processor
1315                .process_transaction(&deposit_tx, &validator)
1316                .success
1317        );
1318        let st_licn = state
1319            .get_mossstake_pool()
1320            .unwrap()
1321            .positions
1322            .get(&alice)
1323            .unwrap()
1324            .st_licn_amount;
1325
1326        put_active_processor_test_restriction(
1327            &state,
1328            RestrictionTarget::Account(alice),
1329            RestrictionMode::OutgoingOnly,
1330        );
1331
1332        let tx = make_mossstake_transfer_tx(&alice_kp, alice, bob, st_licn / 2, genesis_hash);
1333        let result = processor.process_transaction(&tx, &validator);
1334        assert!(!result.success);
1335        assert!(result
1336            .error
1337            .as_deref()
1338            .unwrap_or("")
1339            .contains("MossStakeTransfer blocked by active sender account restriction"));
1340
1341        let pool = state.get_mossstake_pool().unwrap();
1342        assert_eq!(pool.positions.get(&alice).unwrap().st_licn_amount, st_licn);
1343        assert!(!pool.positions.contains_key(&bob));
1344        assert!(state.get_account(&bob).unwrap().is_none());
1345    }
1346
1347    #[test]
1348    fn test_mossstake_transfer_rejects_incoming_restricted_recipient() {
1349        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1350        let validator = Pubkey([42u8; 32]);
1351        let bob = Pubkey([0xB3; 32]);
1352        let deposit_amount = Account::licn_to_spores(100);
1353        let deposit_tx = make_mossstake_deposit_tx(&alice_kp, alice, deposit_amount, genesis_hash);
1354        assert!(
1355            processor
1356                .process_transaction(&deposit_tx, &validator)
1357                .success
1358        );
1359        let st_licn = state
1360            .get_mossstake_pool()
1361            .unwrap()
1362            .positions
1363            .get(&alice)
1364            .unwrap()
1365            .st_licn_amount;
1366
1367        put_active_processor_test_restriction(
1368            &state,
1369            RestrictionTarget::Account(bob),
1370            RestrictionMode::IncomingOnly,
1371        );
1372
1373        let tx = make_mossstake_transfer_tx(&alice_kp, alice, bob, st_licn / 2, genesis_hash);
1374        let result = processor.process_transaction(&tx, &validator);
1375        assert!(!result.success);
1376        assert!(result
1377            .error
1378            .as_deref()
1379            .unwrap_or("")
1380            .contains("MossStakeTransfer blocked by active recipient account restriction"));
1381
1382        let pool = state.get_mossstake_pool().unwrap();
1383        assert_eq!(pool.positions.get(&alice).unwrap().st_licn_amount, st_licn);
1384        assert!(!pool.positions.contains_key(&bob));
1385        assert!(state.get_account(&bob).unwrap().is_none());
1386    }
1387
1388    // ── H16 tests: system instruction types 17, 18, 19 ──
1389
1390    #[test]
1391    fn test_system_deploy_contract_success() {
1392        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1393        let validator = Pubkey([42u8; 32]);
1394
1395        // Fund treasury for test
1396        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1397        treasury_acct
1398            .add_spendable(Account::licn_to_spores(100))
1399            .unwrap();
1400        state.put_account(&treasury, &treasury_acct).unwrap();
1401
1402        // Build deploy instruction: [17 | code_length(4 LE) | code_bytes]
1403        // Valid WASM: magic (4 bytes) + version (4 bytes)
1404        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1405        let mut data = vec![17u8];
1406        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1407        data.extend_from_slice(&code);
1408
1409        let ix = Instruction {
1410            program_id: SYSTEM_PROGRAM_ID,
1411            accounts: vec![alice, treasury],
1412            data,
1413        };
1414        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1415        let mut tx = Transaction::new(message);
1416        let sig = alice_kp.sign(&tx.message.serialize());
1417        tx.signatures.push(sig);
1418
1419        let result = processor.process_transaction(&tx, &validator);
1420        assert!(result.success, "Deploy should succeed: {:?}", result.error);
1421    }
1422
1423    #[test]
1424    fn test_system_deploy_contract_allows_same_code_multiple_times_via_nonce() {
1425        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1426        let validator = Pubkey([42u8; 32]);
1427
1428        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1429        treasury_acct
1430            .add_spendable(Account::licn_to_spores(200))
1431            .unwrap();
1432        state.put_account(&treasury, &treasury_acct).unwrap();
1433
1434        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1435        let build_tx = |blockhash| {
1436            let mut data = vec![17u8];
1437            data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1438            data.extend_from_slice(&code);
1439            let ix = Instruction {
1440                program_id: SYSTEM_PROGRAM_ID,
1441                accounts: vec![alice, treasury],
1442                data,
1443            };
1444            let mut tx = Transaction::new(crate::transaction::Message::new(vec![ix], blockhash));
1445            tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
1446            tx
1447        };
1448
1449        let result1 = processor.process_transaction(&build_tx(genesis_hash), &validator);
1450        assert!(
1451            result1.success,
1452            "first deploy should succeed: {:?}",
1453            result1.error
1454        );
1455
1456        let recent_block = crate::Block::new_with_timestamp(
1457            1,
1458            genesis_hash,
1459            Hash::hash(b"deploy-second-state"),
1460            [0u8; 32],
1461            Vec::new(),
1462            1,
1463        );
1464        let second_hash = recent_block.hash();
1465        state.put_block(&recent_block).unwrap();
1466        state.set_last_slot(1).unwrap();
1467        let result2 = processor.process_transaction(&build_tx(second_hash), &validator);
1468        assert!(
1469            result2.success,
1470            "second deploy should succeed: {:?}",
1471            result2.error
1472        );
1473
1474        let programs = state.get_programs(10).unwrap();
1475        assert_eq!(
1476            programs.len(),
1477            2,
1478            "same code should deploy to two unique addresses"
1479        );
1480        assert_ne!(programs[0], programs[1]);
1481    }
1482
1483    #[test]
1484    fn test_system_deploy_contract_deterministic_mode_rejects_duplicate_address() {
1485        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1486        let validator = Pubkey([42u8; 32]);
1487
1488        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1489        treasury_acct
1490            .add_spendable(Account::licn_to_spores(200))
1491            .unwrap();
1492        state.put_account(&treasury, &treasury_acct).unwrap();
1493
1494        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1495        let init = serde_json::json!({
1496            "name": "deterministic-demo",
1497            "deploy_deterministic": true
1498        })
1499        .to_string();
1500        let build_tx = |blockhash| {
1501            let mut data = vec![17u8];
1502            data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1503            data.extend_from_slice(&code);
1504            data.extend_from_slice(init.as_bytes());
1505            let ix = Instruction {
1506                program_id: SYSTEM_PROGRAM_ID,
1507                accounts: vec![alice, treasury],
1508                data,
1509            };
1510            let mut tx = Transaction::new(crate::transaction::Message::new(vec![ix], blockhash));
1511            tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
1512            tx
1513        };
1514
1515        let result1 = processor.process_transaction(&build_tx(genesis_hash), &validator);
1516        assert!(
1517            result1.success,
1518            "first deterministic deploy should succeed: {:?}",
1519            result1.error
1520        );
1521
1522        let recent_block = crate::Block::new_with_timestamp(
1523            1,
1524            genesis_hash,
1525            Hash::hash(b"deploy-deterministic-second-state"),
1526            [0u8; 32],
1527            Vec::new(),
1528            1,
1529        );
1530        let second_hash = recent_block.hash();
1531        state.put_block(&recent_block).unwrap();
1532        state.set_last_slot(1).unwrap();
1533        let result2 = processor.process_transaction(&build_tx(second_hash), &validator);
1534        assert!(!result2.success, "duplicate deterministic deploy must fail");
1535        assert!(
1536            result2
1537                .error
1538                .as_deref()
1539                .unwrap_or_default()
1540                .contains("Contract already exists"),
1541            "unexpected: {:?}",
1542            result2.error
1543        );
1544    }
1545
1546    /// AUDIT-FIX B-2: System deploy (type 17) charges contract_deploy_fee.
1547    #[test]
1548    fn test_system_deploy_charges_deploy_fee() {
1549        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1550        let validator = Pubkey([42u8; 32]);
1551
1552        // Fund treasury
1553        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1554        treasury_acct
1555            .add_spendable(Account::licn_to_spores(100))
1556            .unwrap();
1557        state.put_account(&treasury, &treasury_acct).unwrap();
1558
1559        let before = state.get_account(&alice).unwrap().unwrap().spendable;
1560
1561        // Valid WASM module
1562        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1563        let mut data = vec![17u8];
1564        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1565        data.extend_from_slice(&code);
1566
1567        let ix = Instruction {
1568            program_id: SYSTEM_PROGRAM_ID,
1569            accounts: vec![alice, treasury],
1570            data,
1571        };
1572        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1573        let mut tx = Transaction::new(message);
1574        let sig = alice_kp.sign(&tx.message.serialize());
1575        tx.signatures.push(sig);
1576
1577        let result = processor.process_transaction(&tx, &validator);
1578        assert!(result.success, "Deploy should succeed: {:?}", result.error);
1579
1580        // The fee should include contract_deploy_fee (25 LICN) + base_fee (0.001 LICN)
1581        let after = state.get_account(&alice).unwrap().unwrap().spendable;
1582        let charged = before - after;
1583        // contract_deploy_fee = 25_000_000_000 spores, base_fee = 1_000_000 spores
1584        assert!(
1585            charged >= 25_000_000_000,
1586            "Expected at least 25 LICN fee for deploy, got {} spores charged",
1587            charged
1588        );
1589    }
1590
1591    /// AUDIT-FIX B-2: An account with only 1 LICN cannot pay the 25 LICN deploy fee.
1592    #[test]
1593    fn test_system_deploy_rejects_underfunded() {
1594        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1595        let validator = Pubkey([42u8; 32]);
1596
1597        // Set Alice to only 1 LICN — cannot afford 25 LICN deploy fee
1598        let low = Account::new(1, alice);
1599        state.put_account(&alice, &low).unwrap();
1600
1601        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1602        let mut data = vec![17u8];
1603        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1604        data.extend_from_slice(&code);
1605
1606        let ix = Instruction {
1607            program_id: SYSTEM_PROGRAM_ID,
1608            accounts: vec![alice, treasury],
1609            data,
1610        };
1611        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1612        let mut tx = Transaction::new(message);
1613        let sig = alice_kp.sign(&tx.message.serialize());
1614        tx.signatures.push(sig);
1615
1616        let result = processor.process_transaction(&tx, &validator);
1617        assert!(
1618            !result.success,
1619            "Deploy with only 1 LICN should fail due to 25 LICN fee"
1620        );
1621    }
1622
1623    #[test]
1624    fn test_system_deploy_contract_invalid_wasm_magic() {
1625        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1626        let validator = Pubkey([42u8; 32]);
1627
1628        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1629        treasury_acct
1630            .add_spendable(Account::licn_to_spores(100))
1631            .unwrap();
1632        state.put_account(&treasury, &treasury_acct).unwrap();
1633
1634        // Invalid magic bytes (not WASM)
1635        let code = vec![0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00];
1636        let mut data = vec![17u8];
1637        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1638        data.extend_from_slice(&code);
1639
1640        let ix = Instruction {
1641            program_id: SYSTEM_PROGRAM_ID,
1642            accounts: vec![alice, treasury],
1643            data,
1644        };
1645        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1646        let mut tx = Transaction::new(message);
1647        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
1648
1649        let result = processor.process_transaction(&tx, &validator);
1650        assert!(
1651            !result.success,
1652            "Deploy with invalid WASM magic should fail"
1653        );
1654        assert!(result.error.unwrap().contains("bad magic number"));
1655    }
1656
1657    #[test]
1658    fn test_system_deploy_contract_too_small() {
1659        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1660        let validator = Pubkey([42u8; 32]);
1661
1662        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1663        treasury_acct
1664            .add_spendable(Account::licn_to_spores(100))
1665            .unwrap();
1666        state.put_account(&treasury, &treasury_acct).unwrap();
1667
1668        // Only 4 bytes — below 8-byte minimum
1669        let code = vec![0x00, 0x61, 0x73, 0x6D];
1670        let mut data = vec![17u8];
1671        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1672        data.extend_from_slice(&code);
1673
1674        let ix = Instruction {
1675            program_id: SYSTEM_PROGRAM_ID,
1676            accounts: vec![alice, treasury],
1677            data,
1678        };
1679        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1680        let mut tx = Transaction::new(message);
1681        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
1682
1683        let result = processor.process_transaction(&tx, &validator);
1684        assert!(!result.success, "Deploy with code too small should fail");
1685        assert!(result.error.unwrap().contains("too small"));
1686    }
1687
1688    #[test]
1689    fn test_code_hash_deploy_block_rejects_system_deploy_contract() {
1690        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1691        let validator = Pubkey([42u8; 32]);
1692
1693        let mut treasury_acct = state.get_account(&treasury).unwrap().unwrap();
1694        treasury_acct
1695            .add_spendable(Account::licn_to_spores(100))
1696            .unwrap();
1697        state.put_account(&treasury, &treasury_acct).unwrap();
1698
1699        let code = valid_wasm_code(0x40);
1700        let code_hash = Hash::hash(&code);
1701        let restriction_id = put_active_processor_test_restriction(
1702            &state,
1703            RestrictionTarget::CodeHash(code_hash),
1704            RestrictionMode::DeployBlocked,
1705        );
1706
1707        let mut data = vec![17u8];
1708        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1709        data.extend_from_slice(&code);
1710        let ix = Instruction {
1711            program_id: SYSTEM_PROGRAM_ID,
1712            accounts: vec![alice, treasury],
1713            data,
1714        };
1715        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
1716
1717        let result = processor.process_transaction(&tx, &validator);
1718        assert!(!result.success, "Banned code hash deploy must fail");
1719        let error = result.error.as_deref().unwrap_or_default();
1720        assert!(error.contains("DeployContract rejected"));
1721        assert!(error.contains("DeployBlocked"));
1722        assert!(error.contains(&restriction_id.to_string()));
1723        assert!(
1724            state.get_programs(10).unwrap().is_empty(),
1725            "blocked deploy must not index a program"
1726        );
1727    }
1728
1729    #[test]
1730    fn test_code_hash_deploy_block_rejects_contract_program_deploy() {
1731        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1732        let validator = Pubkey([42u8; 32]);
1733
1734        let code = valid_wasm_code(0x41);
1735        let code_hash = Hash::hash(&code);
1736        let restriction_id = put_active_processor_test_restriction(
1737            &state,
1738            RestrictionTarget::CodeHash(code_hash),
1739            RestrictionMode::DeployBlocked,
1740        );
1741        let contract_addr = Pubkey([0x41; 32]);
1742
1743        let result = submit_contract_ix(
1744            &processor,
1745            &alice_kp,
1746            vec![alice, contract_addr],
1747            crate::ContractInstruction::Deploy {
1748                code,
1749                init_data: Vec::new(),
1750            },
1751            genesis_hash,
1752            &validator,
1753        );
1754        assert!(!result.success, "Banned code hash deploy must fail");
1755        let error = result.error.as_deref().unwrap_or_default();
1756        assert!(error.contains("Deploy rejected"));
1757        assert!(error.contains("DeployBlocked"));
1758        assert!(error.contains(&restriction_id.to_string()));
1759        assert!(
1760            state.get_account(&contract_addr).unwrap().is_none(),
1761            "blocked deploy must not create a contract account"
1762        );
1763    }
1764
1765    #[test]
1766    fn test_simulate_code_hash_deploy_block_rejects_contract_program_deploy() {
1767        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1768
1769        let code = valid_wasm_code(0x42);
1770        let code_hash = Hash::hash(&code);
1771        let restriction_id = put_active_processor_test_restriction(
1772            &state,
1773            RestrictionTarget::CodeHash(code_hash),
1774            RestrictionMode::DeployBlocked,
1775        );
1776        let contract_addr = Pubkey([0x42; 32]);
1777        let ix = Instruction {
1778            program_id: CONTRACT_PROGRAM_ID,
1779            accounts: vec![alice, contract_addr],
1780            data: crate::ContractInstruction::Deploy {
1781                code,
1782                init_data: Vec::new(),
1783            }
1784            .serialize()
1785            .unwrap(),
1786        };
1787        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
1788
1789        let sim = processor.simulate_transaction(&tx);
1790        assert!(
1791            !sim.success,
1792            "simulation must reject deploys for banned code hashes"
1793        );
1794        let error = sim.error.as_deref().unwrap_or_default();
1795        assert!(error.contains("Deploy rejected"));
1796        assert!(error.contains("DeployBlocked"));
1797        assert!(error.contains(&restriction_id.to_string()));
1798        assert!(
1799            state.get_account(&contract_addr).unwrap().is_none(),
1800            "blocked simulation must not create a contract account"
1801        );
1802    }
1803
1804    #[test]
1805    fn test_simulate_code_hash_deploy_block_rejects_system_deploy_contract() {
1806        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
1807
1808        let code = valid_wasm_code(0x43);
1809        let code_hash = Hash::hash(&code);
1810        let restriction_id = put_active_processor_test_restriction(
1811            &state,
1812            RestrictionTarget::CodeHash(code_hash),
1813            RestrictionMode::DeployBlocked,
1814        );
1815        let mut data = vec![17u8];
1816        data.extend_from_slice(&(code.len() as u32).to_le_bytes());
1817        data.extend_from_slice(&code);
1818        let ix = Instruction {
1819            program_id: SYSTEM_PROGRAM_ID,
1820            accounts: vec![alice, treasury],
1821            data,
1822        };
1823        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
1824
1825        let sim = processor.simulate_transaction(&tx);
1826        assert!(
1827            !sim.success,
1828            "simulation must reject system deploys for banned code hashes"
1829        );
1830        let error = sim.error.as_deref().unwrap_or_default();
1831        assert!(error.contains("DeployContract rejected"));
1832        assert!(error.contains("DeployBlocked"));
1833        assert!(error.contains(&restriction_id.to_string()));
1834        assert!(
1835            state.get_programs(10).unwrap().is_empty(),
1836            "blocked simulation must not index a program"
1837        );
1838    }
1839
1840    /// Test: ContractInstruction::Deploy via CONTRACT_PROGRAM_ID with init_data
1841    /// populates the symbol registry atomically.
1842    #[test]
1843    fn test_contract_program_deploy_with_symbol_registry() {
1844        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1845        let validator = Pubkey([42u8; 32]);
1846
1847        // Valid WASM module (magic + version)
1848        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1849
1850        // Build init_data JSON with symbol registration metadata
1851        let init_data = serde_json::json!({
1852            "symbol": "TESTCOIN",
1853            "name": "Test Coin",
1854            "template": "token",
1855            "decimals": 9,
1856            "metadata": {
1857                "description": "A test token for unit testing",
1858                "website": "https://example.com",
1859                "mintable": "true"
1860            }
1861        });
1862        let init_data_bytes = serde_json::to_vec(&init_data).unwrap();
1863
1864        // Compute contract address like the CLI does
1865        let code_hash = Hash::hash(&code);
1866        let mut addr_bytes = [0u8; 32];
1867        addr_bytes[..16].copy_from_slice(&alice.0[..16]);
1868        addr_bytes[16..].copy_from_slice(&code_hash.0[..16]);
1869        let contract_addr = Pubkey(addr_bytes);
1870
1871        // Create deploy instruction via CONTRACT_PROGRAM_ID
1872        let contract_ix = crate::ContractInstruction::Deploy {
1873            code: code.clone(),
1874            init_data: init_data_bytes.clone(),
1875        };
1876        let ix = Instruction {
1877            program_id: CONTRACT_PROGRAM_ID,
1878            accounts: vec![alice, contract_addr],
1879            data: contract_ix.serialize().unwrap(),
1880        };
1881
1882        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1883        let mut tx = Transaction::new(message);
1884        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
1885
1886        let result = processor.process_transaction(&tx, &validator);
1887        assert!(
1888            result.success,
1889            "ContractProgram Deploy should succeed: {:?}",
1890            result.error
1891        );
1892
1893        // Verify contract account exists and is executable
1894        let acct = state.get_account(&contract_addr).unwrap();
1895        assert!(acct.is_some(), "Contract account should exist");
1896        assert!(acct.unwrap().executable, "Contract should be executable");
1897
1898        // Verify symbol registry entry was written
1899        let entry = state.get_symbol_registry("TESTCOIN").unwrap();
1900        assert!(
1901            entry.is_some(),
1902            "Symbol TESTCOIN should be in the registry after deploy"
1903        );
1904        let entry = entry.unwrap();
1905        assert_eq!(entry.symbol, "TESTCOIN");
1906        assert_eq!(entry.program, contract_addr);
1907        assert_eq!(entry.owner, alice);
1908        assert_eq!(entry.name, Some("Test Coin".to_string()));
1909        assert_eq!(entry.template, Some("token".to_string()));
1910        assert_eq!(entry.decimals, Some(9));
1911        assert!(entry.metadata.is_some());
1912        let meta = entry.metadata.unwrap();
1913        assert_eq!(
1914            meta.get("description").and_then(|v| v.as_str()),
1915            Some("A test token for unit testing")
1916        );
1917    }
1918
1919    #[test]
1920    fn test_validate_and_sanitize_metadata_accepts_scalar_values() {
1921        let metadata = Some(serde_json::json!({
1922            "description": "Community token",
1923            "decimals": 9,
1924            "mintable": true,
1925            "burnable": false
1926        }));
1927
1928        let sanitized = TxProcessor::validate_and_sanitize_metadata(&metadata)
1929            .expect("scalar metadata should be accepted")
1930            .expect("metadata should remain present");
1931
1932        assert_eq!(
1933            sanitized
1934                .get("description")
1935                .and_then(|value| value.as_str()),
1936            Some("Community token")
1937        );
1938        assert_eq!(
1939            sanitized.get("decimals").and_then(|value| value.as_u64()),
1940            Some(9)
1941        );
1942        assert_eq!(
1943            sanitized.get("mintable").and_then(|value| value.as_bool()),
1944            Some(true)
1945        );
1946        assert_eq!(
1947            sanitized.get("burnable").and_then(|value| value.as_bool()),
1948            Some(false)
1949        );
1950    }
1951
1952    #[test]
1953    fn test_validate_and_sanitize_metadata_rejects_nested_values() {
1954        let metadata = Some(serde_json::json!({
1955            "social_urls": {
1956                "twitter": "https://x.com/lichen"
1957            }
1958        }));
1959
1960        let err = TxProcessor::validate_and_sanitize_metadata(&metadata)
1961            .expect_err("nested metadata must be rejected");
1962        assert!(err.contains("string, number, or boolean"));
1963    }
1964
1965    /// Test: Deploy fee premium is refunded when deploy instruction itself fails.
1966    #[test]
1967    fn test_contract_program_deploy_failure_refunds_premium() {
1968        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
1969        let validator = Pubkey([42u8; 32]);
1970
1971        let initial_balance = state.get_balance(&alice).unwrap();
1972
1973        // Invalid WASM (bad magic bytes) — deploy should fail
1974        let bad_code = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x00, 0x00, 0x00];
1975
1976        let code_hash = Hash::hash(&bad_code);
1977        let mut addr_bytes = [0u8; 32];
1978        addr_bytes[..16].copy_from_slice(&alice.0[..16]);
1979        addr_bytes[16..].copy_from_slice(&code_hash.0[..16]);
1980        let contract_addr = Pubkey(addr_bytes);
1981
1982        let contract_ix = crate::ContractInstruction::Deploy {
1983            code: bad_code,
1984            init_data: vec![],
1985        };
1986        let ix = Instruction {
1987            program_id: CONTRACT_PROGRAM_ID,
1988            accounts: vec![alice, contract_addr],
1989            data: contract_ix.serialize().unwrap(),
1990        };
1991
1992        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
1993        let mut tx = Transaction::new(message);
1994        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
1995
1996        let result = processor.process_transaction(&tx, &validator);
1997        assert!(!result.success, "Deploy with bad WASM should fail");
1998
1999        // Verify only base fee was kept (premium refunded)
2000        let final_balance = state.get_balance(&alice).unwrap();
2001        let fee_kept = initial_balance - final_balance;
2002        // base_fee = 1_000_000 spores (0.001 LICN), deploy premium = 25_000_000_000
2003        assert!(
2004            fee_kept < 25_000_000_000,
2005            "Premium should be refunded on failed deploy, but {} spores kept",
2006            fee_kept
2007        );
2008    }
2009
2010    #[test]
2011    fn test_failed_premium_fee_refund_bypasses_incoming_restriction() {
2012        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
2013        let validator = Pubkey([42u8; 32]);
2014
2015        put_active_processor_test_restriction(
2016            &state,
2017            RestrictionTarget::Account(alice),
2018            RestrictionMode::IncomingOnly,
2019        );
2020        put_active_processor_test_restriction(
2021            &state,
2022            RestrictionTarget::AccountAsset {
2023                account: alice,
2024                asset: NATIVE_LICN_ASSET_ID,
2025            },
2026            RestrictionMode::IncomingOnly,
2027        );
2028
2029        let initial_balance = state.get_balance(&alice).unwrap();
2030        let bad_code = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x00, 0x00, 0x00];
2031        let code_hash = Hash::hash(&bad_code);
2032        let mut addr_bytes = [0u8; 32];
2033        addr_bytes[..16].copy_from_slice(&alice.0[..16]);
2034        addr_bytes[16..].copy_from_slice(&code_hash.0[..16]);
2035        let contract_addr = Pubkey(addr_bytes);
2036
2037        let ix = Instruction {
2038            program_id: CONTRACT_PROGRAM_ID,
2039            accounts: vec![alice, contract_addr],
2040            data: crate::ContractInstruction::Deploy {
2041                code: bad_code,
2042                init_data: vec![],
2043            }
2044            .serialize()
2045            .unwrap(),
2046        };
2047        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
2048        let result = processor.process_transaction(&tx, &validator);
2049        assert!(!result.success, "Deploy with bad WASM should fail");
2050        assert!(result
2051            .error
2052            .as_deref()
2053            .unwrap_or("")
2054            .contains("bad magic number"));
2055
2056        let final_balance = state.get_balance(&alice).unwrap();
2057        assert_eq!(initial_balance - final_balance, result.fee_paid);
2058        assert_eq!(result.fee_paid, BASE_FEE);
2059    }
2060
2061    #[test]
2062    fn test_system_set_contract_abi() {
2063        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
2064        let validator = Pubkey([42u8; 32]);
2065
2066        // First deploy a contract
2067        // Valid WASM: magic (4 bytes) + version (4 bytes)
2068        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
2069        let mut deploy_data = vec![17u8];
2070        deploy_data.extend_from_slice(&(code.len() as u32).to_le_bytes());
2071        deploy_data.extend_from_slice(&code);
2072
2073        let deploy_ix = Instruction {
2074            program_id: SYSTEM_PROGRAM_ID,
2075            accounts: vec![alice, treasury],
2076            data: deploy_data.clone(),
2077        };
2078        let msg = crate::transaction::Message::new(vec![deploy_ix], genesis_hash);
2079        let mut tx = Transaction::new(msg);
2080        let sig = alice_kp.sign(&tx.message.serialize());
2081        tx.signatures.push(sig);
2082        let r = processor.process_transaction(&tx, &validator);
2083        assert!(
2084            r.success,
2085            "Deploy for ABI test should succeed: {:?}",
2086            r.error
2087        );
2088
2089        let programs = state.get_programs(10).unwrap();
2090        assert_eq!(programs.len(), 1, "expected a single deployed program");
2091        let program_pubkey = programs[0];
2092
2093        // Now set ABI
2094        let abi = serde_json::json!({
2095            "version": "1.0",
2096            "name": "TestContract",
2097            "functions": []
2098        });
2099        let abi_bytes = serde_json::to_vec(&abi).unwrap();
2100        let mut abi_data = vec![18u8];
2101        abi_data.extend_from_slice(&abi_bytes);
2102
2103        let abi_ix = Instruction {
2104            program_id: SYSTEM_PROGRAM_ID,
2105            accounts: vec![alice, program_pubkey],
2106            data: abi_data,
2107        };
2108        let msg2 = crate::transaction::Message::new(vec![abi_ix], genesis_hash);
2109        let mut tx2 = Transaction::new(msg2);
2110        let sig2 = alice_kp.sign(&tx2.message.serialize());
2111        tx2.signatures.push(sig2);
2112        let result = processor.process_transaction(&tx2, &validator);
2113        assert!(
2114            result.success,
2115            "SetContractAbi should succeed: {:?}",
2116            result.error
2117        );
2118
2119        // Verify ABI is stored
2120        let acct = state.get_account(&program_pubkey).unwrap().unwrap();
2121        let contract: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
2122        assert!(contract.abi.is_some());
2123    }
2124
2125    #[test]
2126    fn test_system_set_contract_abi_wrong_owner_fails() {
2127        let (processor, state, alice_kp, alice, treasury, genesis_hash) = setup();
2128        let validator = Pubkey([42u8; 32]);
2129
2130        // Deploy a contract as alice
2131        // Valid WASM: magic (4 bytes) + version (4 bytes)
2132        let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
2133        let mut deploy_data = vec![17u8];
2134        deploy_data.extend_from_slice(&(code.len() as u32).to_le_bytes());
2135        deploy_data.extend_from_slice(&code);
2136        let deploy_ix = Instruction {
2137            program_id: SYSTEM_PROGRAM_ID,
2138            accounts: vec![alice, treasury],
2139            data: deploy_data,
2140        };
2141        let msg = crate::transaction::Message::new(vec![deploy_ix], genesis_hash);
2142        let mut tx = Transaction::new(msg);
2143        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
2144        assert!(processor.process_transaction(&tx, &validator).success);
2145
2146        let programs = state.get_programs(10).unwrap();
2147        assert_eq!(programs.len(), 1, "expected a single deployed program");
2148        let program_pubkey = programs[0];
2149
2150        // Try setting ABI as a different user (bob)
2151        let bob_kp = Keypair::generate();
2152        let bob = bob_kp.pubkey();
2153        state.put_account(&bob, &Account::new(100, bob)).unwrap();
2154
2155        let abi_bytes = b"{\"version\":\"1.0\"}";
2156        let mut abi_data = vec![18u8];
2157        abi_data.extend_from_slice(abi_bytes);
2158        let abi_ix = Instruction {
2159            program_id: SYSTEM_PROGRAM_ID,
2160            accounts: vec![bob, program_pubkey],
2161            data: abi_data,
2162        };
2163        let msg2 = crate::transaction::Message::new(vec![abi_ix], genesis_hash);
2164        let mut tx2 = Transaction::new(msg2);
2165        tx2.signatures.push(bob_kp.sign(&tx2.message.serialize()));
2166        let r = processor.process_transaction(&tx2, &validator);
2167        assert!(!r.success, "SetContractAbi by non-owner should fail");
2168    }
2169
2170    #[test]
2171    fn test_system_faucet_airdrop() {
2172        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
2173        let validator = Pubkey([42u8; 32]);
2174
2175        // Fund treasury
2176        let mut t = state.get_account(&treasury).unwrap().unwrap();
2177        t.add_spendable(Account::licn_to_spores(1000)).unwrap();
2178        state.put_account(&treasury, &t).unwrap();
2179
2180        let recipient = Pubkey([0x99; 32]);
2181        let amount: u64 = Account::licn_to_spores(10);
2182
2183        let mut data = vec![19u8];
2184        data.extend_from_slice(&amount.to_le_bytes());
2185
2186        let _ix = Instruction {
2187            program_id: SYSTEM_PROGRAM_ID,
2188            accounts: vec![treasury, recipient],
2189            data,
2190        };
2191        // Faucet airdrop needs to be signed by treasury — we use a keypair for the test
2192        let treasury_kp = Keypair::from_seed(&[3u8; 32]);
2193        // Re-set treasury pubkey to match the keyed treasury
2194        state.set_treasury_pubkey(&treasury_kp.pubkey()).unwrap();
2195        let treasury_pk = treasury_kp.pubkey();
2196        let tacct = state.get_account(&treasury).unwrap().unwrap();
2197        state.put_account(&treasury_pk, &tacct).unwrap();
2198
2199        let ix2 = Instruction {
2200            program_id: SYSTEM_PROGRAM_ID,
2201            accounts: vec![treasury_pk, recipient],
2202            data: {
2203                let mut d = vec![19u8];
2204                d.extend_from_slice(&amount.to_le_bytes());
2205                d
2206            },
2207        };
2208        let msg = crate::transaction::Message::new(vec![ix2], genesis_hash);
2209        let mut tx = Transaction::new(msg);
2210        tx.signatures
2211            .push(treasury_kp.sign(&tx.message.serialize()));
2212        let result = processor.process_transaction(&tx, &validator);
2213        assert!(
2214            result.success,
2215            "Faucet airdrop should succeed: {:?}",
2216            result.error
2217        );
2218
2219        let r = state.get_account(&recipient).unwrap();
2220        assert!(r.is_some());
2221        assert_eq!(r.unwrap().spendable, amount);
2222    }
2223
2224    #[test]
2225    fn test_fee_split_no_overflow_large_values() {
2226        // L6-01: Verify u128 intermediate prevents overflow when fee * percent > u64::MAX
2227        let (processor, state, _alice_kp, alice, treasury, _genesis_hash) = setup();
2228
2229        // Give alice a huge balance
2230        let mut a = state.get_account(&alice).unwrap().unwrap();
2231        let initial_spendable = a.spendable;
2232        a.add_spendable(u64::MAX / 2).unwrap();
2233        state.put_account(&alice, &a).unwrap();
2234
2235        // A fee of 1e18 (~1e9 LICN) times percent 50 would overflow u64 multiply
2236        let large_fee: u64 = 1_000_000_000_000_000_000; // 1e18 spores
2237        let result = processor.charge_fee_direct(&alice, large_fee);
2238        assert!(
2239            result.is_ok(),
2240            "Large fee should not overflow: {:?}",
2241            result.err()
2242        );
2243
2244        // Verify payer was debited
2245        let a_after = state.get_account(&alice).unwrap().unwrap();
2246        assert_eq!(
2247            a_after.spendable,
2248            initial_spendable + u64::MAX / 2 - large_fee,
2249            "Payer should be debited exactly the fee amount"
2250        );
2251
2252        // Verify treasury received the non-burned portion
2253        let t = state.get_account(&treasury).unwrap().unwrap();
2254        assert!(t.spendable > 0, "Treasury should have received fee portion");
2255    }
2256
2257    #[test]
2258    fn test_system_faucet_airdrop_cap_exceeded() {
2259        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
2260        let validator = Pubkey([42u8; 32]);
2261
2262        let mut t = state.get_account(&treasury).unwrap().unwrap();
2263        t.add_spendable(Account::licn_to_spores(10000)).unwrap();
2264        state.put_account(&treasury, &t).unwrap();
2265
2266        let recipient = Pubkey([0xBB; 32]);
2267        // 200 LICN exceeds 10 LICN cap
2268        let amount: u64 = 200u64 * 1_000_000_000;
2269
2270        let mut data = vec![19u8];
2271        data.extend_from_slice(&amount.to_le_bytes());
2272
2273        let _ix = Instruction {
2274            program_id: SYSTEM_PROGRAM_ID,
2275            accounts: vec![treasury, recipient],
2276            data,
2277        };
2278        let treasury_kp = Keypair::from_seed(&[3u8; 32]);
2279        state.set_treasury_pubkey(&treasury_kp.pubkey()).unwrap();
2280        state.put_account(&treasury_kp.pubkey(), &t).unwrap();
2281
2282        let ix2 = Instruction {
2283            program_id: SYSTEM_PROGRAM_ID,
2284            accounts: vec![treasury_kp.pubkey(), recipient],
2285            data: {
2286                let mut d = vec![19u8];
2287                d.extend_from_slice(&amount.to_le_bytes());
2288                d
2289            },
2290        };
2291        let msg = crate::transaction::Message::new(vec![ix2], genesis_hash);
2292        let mut tx = Transaction::new(msg);
2293        tx.signatures
2294            .push(treasury_kp.sign(&tx.message.serialize()));
2295        let result = processor.process_transaction(&tx, &validator);
2296        assert!(!result.success, "Airdrop > 10 LICN should fail");
2297    }
2298
2299    // ═════════════════════════════════════════════════════════════════════════
2300    // K1-01: Parallel transaction processing & conflict detection tests
2301    // ═════════════════════════════════════════════════════════════════════════
2302
2303    #[test]
2304    fn test_parallel_disjoint_txs_succeed() {
2305        // Two transfers to different recipients should both succeed in parallel
2306        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
2307        let bob = Pubkey([2u8; 32]);
2308        let carol = Pubkey([4u8; 32]);
2309        let validator = Pubkey([42u8; 32]);
2310
2311        // Fund alice enough for both transfers + fees
2312        let alice_account = Account::new(500, alice);
2313        state.put_account(&alice, &alice_account).unwrap();
2314
2315        // Both txs FROM alice → different targets: they SHARE alice and will be in same group
2316        let tx1 = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
2317        let tx2 = make_transfer_tx(&alice_kp, alice, carol, 10, genesis_hash);
2318
2319        let results = processor.process_transactions_parallel(&[tx1, tx2], &validator);
2320        assert_eq!(results.len(), 2);
2321        assert!(
2322            results[0].success,
2323            "tx1 (alice→bob) should succeed: {:?}",
2324            results[0].error
2325        );
2326        assert!(
2327            results[1].success,
2328            "tx2 (alice→carol) should succeed: {:?}",
2329            results[1].error
2330        );
2331    }
2332
2333    #[test]
2334    fn test_parallel_truly_disjoint_txs() {
2335        // Two completely independent senders → should run in separate parallel groups
2336        let temp_dir = tempdir().unwrap();
2337        let state = StateStore::open(temp_dir.path()).unwrap();
2338        let processor = TxProcessor::new(state.clone());
2339        let validator = Pubkey([42u8; 32]);
2340
2341        let alice_kp = Keypair::generate();
2342        let alice = alice_kp.pubkey();
2343        let bob_kp = Keypair::generate();
2344        let bob = bob_kp.pubkey();
2345        let carol = Pubkey([4u8; 32]);
2346        let dave = Pubkey([5u8; 32]);
2347        let treasury = Pubkey([3u8; 32]);
2348
2349        state.set_treasury_pubkey(&treasury).unwrap();
2350        state
2351            .put_account(&treasury, &Account::new(0, treasury))
2352            .unwrap();
2353        state
2354            .put_account(&alice, &Account::new(500, alice))
2355            .unwrap();
2356        state.put_account(&bob, &Account::new(500, bob)).unwrap();
2357
2358        let genesis = crate::Block::new_with_timestamp(
2359            0,
2360            Hash::default(),
2361            Hash::default(),
2362            [0u8; 32],
2363            Vec::new(),
2364            0,
2365        );
2366        state.put_block(&genesis).unwrap();
2367        state.set_last_slot(0).unwrap();
2368        let genesis_hash = genesis.hash();
2369
2370        // alice→carol and bob→dave are fully disjoint — parallel groups
2371        let tx1 = make_transfer_tx(&alice_kp, alice, carol, 10, genesis_hash);
2372        let tx2 = make_transfer_tx(&bob_kp, bob, dave, 10, genesis_hash);
2373
2374        let results = processor.process_transactions_parallel(&[tx1, tx2], &validator);
2375        assert_eq!(results.len(), 2);
2376        assert!(
2377            results[0].success,
2378            "alice→carol should succeed: {:?}",
2379            results[0].error
2380        );
2381        assert!(
2382            results[1].success,
2383            "bob→dave should succeed: {:?}",
2384            results[1].error
2385        );
2386    }
2387
2388    #[test]
2389    fn test_parallel_fee_charging_preserves_all_treasury_credits() {
2390        let temp_dir = tempdir().unwrap();
2391        let state = StateStore::open(temp_dir.path()).unwrap();
2392        let processor = TxProcessor::new(state.clone());
2393        let validator = Pubkey([42u8; 32]);
2394        let treasury = Pubkey([3u8; 32]);
2395
2396        state.set_treasury_pubkey(&treasury).unwrap();
2397        state
2398            .put_account(&treasury, &Account::new(0, treasury))
2399            .unwrap();
2400
2401        let genesis = crate::Block::new_with_timestamp(
2402            0,
2403            Hash::default(),
2404            Hash::default(),
2405            [0u8; 32],
2406            Vec::new(),
2407            0,
2408        );
2409        state.put_block(&genesis).unwrap();
2410        state.set_last_slot(0).unwrap();
2411        let genesis_hash = genesis.hash();
2412
2413        let tx_count = 128usize;
2414        let mut txs = Vec::with_capacity(tx_count);
2415        for i in 0..tx_count {
2416            let payer_kp = Keypair::generate();
2417            let payer = payer_kp.pubkey();
2418            let mut recipient_bytes = [0x80u8; 32];
2419            recipient_bytes[..8].copy_from_slice(&(i as u64).to_le_bytes());
2420            let recipient = Pubkey(recipient_bytes);
2421            state
2422                .put_account(&payer, &Account::new(100, payer))
2423                .unwrap();
2424            txs.push(make_transfer_tx(
2425                &payer_kp,
2426                payer,
2427                recipient,
2428                1,
2429                genesis_hash,
2430            ));
2431        }
2432
2433        let results = processor.process_transactions_parallel(&txs, &validator);
2434        for (idx, result) in results.iter().enumerate() {
2435            assert!(
2436                result.success,
2437                "parallel fee tx {} failed: {:?}",
2438                idx, result.error
2439            );
2440        }
2441
2442        let fee_config = FeeConfig::default_from_constants();
2443        let burned_per_tx =
2444            (fee_config.base_fee as u128 * fee_config.fee_burn_percent as u128 / 100) as u64;
2445        let expected_treasury_per_tx = fee_config.base_fee.saturating_sub(burned_per_tx);
2446        assert_eq!(
2447            state.get_balance(&treasury).unwrap(),
2448            expected_treasury_per_tx * tx_count as u64,
2449            "parallel fee charging must not lose treasury credits"
2450        );
2451    }
2452
2453    #[test]
2454    fn test_parallel_conflicting_txs_sequential() {
2455        // Two senders sending TO the same recipient share an account
2456        // They should still both succeed (processed sequentially within group)
2457        let temp_dir = tempdir().unwrap();
2458        let state = StateStore::open(temp_dir.path()).unwrap();
2459        let processor = TxProcessor::new(state.clone());
2460        let validator = Pubkey([42u8; 32]);
2461
2462        let alice_kp = Keypair::generate();
2463        let alice = alice_kp.pubkey();
2464        let bob_kp = Keypair::generate();
2465        let bob = bob_kp.pubkey();
2466        let shared_recipient = Pubkey([99u8; 32]);
2467        let treasury = Pubkey([3u8; 32]);
2468
2469        state.set_treasury_pubkey(&treasury).unwrap();
2470        state
2471            .put_account(&treasury, &Account::new(0, treasury))
2472            .unwrap();
2473        state
2474            .put_account(&alice, &Account::new(500, alice))
2475            .unwrap();
2476        state.put_account(&bob, &Account::new(500, bob)).unwrap();
2477
2478        let genesis = crate::Block::new_with_timestamp(
2479            0,
2480            Hash::default(),
2481            Hash::default(),
2482            [0u8; 32],
2483            Vec::new(),
2484            0,
2485        );
2486        state.put_block(&genesis).unwrap();
2487        state.set_last_slot(0).unwrap();
2488        let genesis_hash = genesis.hash();
2489
2490        // Both send to shared_recipient → merged into same group
2491        let tx1 = make_transfer_tx(&alice_kp, alice, shared_recipient, 10, genesis_hash);
2492        let tx2 = make_transfer_tx(&bob_kp, bob, shared_recipient, 10, genesis_hash);
2493
2494        let results = processor.process_transactions_parallel(&[tx1, tx2], &validator);
2495        assert_eq!(results.len(), 2);
2496        assert!(
2497            results[0].success,
2498            "tx1 should succeed in sequential group: {:?}",
2499            results[0].error
2500        );
2501        assert!(
2502            results[1].success,
2503            "tx2 should succeed in sequential group: {:?}",
2504            results[1].error
2505        );
2506
2507        // Verify both actually transferred
2508        let r = state.get_account(&shared_recipient).unwrap().unwrap();
2509        let alice_sent = Account::licn_to_spores(10);
2510        let bob_sent = Account::licn_to_spores(10);
2511        assert!(
2512            r.spendable >= alice_sent + bob_sent,
2513            "Recipient should have both transfers"
2514        );
2515    }
2516
2517    #[test]
2518    fn test_parallel_result_ordering_preserved() {
2519        // Ensure results[i] corresponds to txs[i] even when groups are reordered
2520        let temp_dir = tempdir().unwrap();
2521        let state = StateStore::open(temp_dir.path()).unwrap();
2522        let processor = TxProcessor::new(state.clone());
2523        let validator = Pubkey([42u8; 32]);
2524
2525        let treasury = Pubkey([3u8; 32]);
2526        state.set_treasury_pubkey(&treasury).unwrap();
2527        state
2528            .put_account(&treasury, &Account::new(0, treasury))
2529            .unwrap();
2530
2531        let genesis = crate::Block::new_with_timestamp(
2532            0,
2533            Hash::default(),
2534            Hash::default(),
2535            [0u8; 32],
2536            Vec::new(),
2537            0,
2538        );
2539        state.put_block(&genesis).unwrap();
2540        state.set_last_slot(0).unwrap();
2541        let genesis_hash = genesis.hash();
2542
2543        // Create 4 independent senders for 4 disjoint txs
2544        let mut txs = Vec::new();
2545        let mut kps = Vec::new();
2546        for i in 0..4u8 {
2547            let kp = Keypair::generate();
2548            let pk = kp.pubkey();
2549            state.put_account(&pk, &Account::new(100, pk)).unwrap();
2550            let recipient = Pubkey([100 + i; 32]);
2551            txs.push(make_transfer_tx(&kp, pk, recipient, 5, genesis_hash));
2552            kps.push(kp);
2553        }
2554
2555        let results = processor.process_transactions_parallel(&txs, &validator);
2556        assert_eq!(results.len(), 4);
2557        for (i, res) in results.iter().enumerate() {
2558            assert!(res.success, "tx[{}] should succeed: {:?}", i, res.error);
2559        }
2560    }
2561
2562    #[test]
2563    fn test_parallel_single_tx_fallback() {
2564        // A single transaction should work fine (no parallelism needed)
2565        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
2566        let bob = Pubkey([2u8; 32]);
2567        let validator = Pubkey([42u8; 32]);
2568
2569        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
2570        let results = processor.process_transactions_parallel(&[tx], &validator);
2571        assert_eq!(results.len(), 1);
2572        assert!(
2573            results[0].success,
2574            "Single tx should succeed: {:?}",
2575            results[0].error
2576        );
2577    }
2578
2579    #[test]
2580    fn test_parallel_empty_batch() {
2581        let (processor, _state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
2582        let validator = Pubkey([42u8; 32]);
2583        let results = processor.process_transactions_parallel(&[], &validator);
2584        assert_eq!(results.len(), 0);
2585    }
2586
2587    /// P9-RPC-01: Non-EVM TXs with the EVM sentinel blockhash must be rejected.
2588    #[test]
2589    fn test_sentinel_blockhash_rejected_for_non_evm_tx() {
2590        let (processor, _state, alice_kp, alice, _treasury, _genesis_hash) = setup();
2591        let validator = Pubkey([42u8; 32]);
2592
2593        // Build a normal transfer using the sentinel blockhash
2594        let ix = crate::transaction::Instruction {
2595            program_id: SYSTEM_PROGRAM_ID,
2596            accounts: vec![alice, Pubkey([5u8; 32])],
2597            data: {
2598                let mut d = vec![0u8]; // Transfer
2599                d.extend_from_slice(&100u64.to_le_bytes());
2600                d
2601            },
2602        };
2603        let msg = crate::transaction::Message {
2604            instructions: vec![ix],
2605            recent_blockhash: EVM_SENTINEL_BLOCKHASH,
2606            compute_budget: None,
2607            compute_unit_price: None,
2608        };
2609        let sig = alice_kp.sign(&msg.serialize());
2610        let tx = Transaction {
2611            signatures: vec![sig],
2612            message: msg,
2613            tx_type: Default::default(),
2614        };
2615        let result = processor.process_transaction(&tx, &validator);
2616        assert!(
2617            !result.success,
2618            "Non-EVM TX with sentinel blockhash should be rejected"
2619        );
2620        assert!(
2621            result
2622                .error
2623                .as_deref()
2624                .unwrap_or("")
2625                .contains("EVM sentinel blockhash"),
2626            "Error should mention the sentinel: {:?}",
2627            result.error,
2628        );
2629    }
2630
2631    /// P9-RPC-01: EVM TX with sentinel blockhash must be accepted (routed to EVM path).
2632    /// It will fail at the EVM decode stage (no valid RLP in dummy data) but must
2633    /// NOT be rejected at the sentinel/blockhash check itself.
2634    #[test]
2635    fn test_sentinel_blockhash_accepted_for_evm_tx() {
2636        let (processor, _state, _alice_kp, alice, _treasury, _genesis_hash) = setup();
2637        let validator = Pubkey([42u8; 32]);
2638
2639        // Build an EVM-program TX with sentinel blockhash and dummy data
2640        let ix = crate::transaction::Instruction {
2641            program_id: crate::evm::EVM_PROGRAM_ID,
2642            accounts: vec![alice],
2643            data: vec![0xDE, 0xAD], // invalid EVM payload — will fail decoding, not sentinel check
2644        };
2645        let msg = crate::transaction::Message {
2646            instructions: vec![ix],
2647            recent_blockhash: EVM_SENTINEL_BLOCKHASH,
2648            compute_budget: None,
2649            compute_unit_price: None,
2650        };
2651        let tx = Transaction {
2652            signatures: vec![crate::PqSignature::test_fixture(0)],
2653            message: msg,
2654            tx_type: Default::default(),
2655        };
2656        let result = processor.process_transaction(&tx, &validator);
2657        // Should fail with EVM decode error — NOT with "sentinel blockhash" error
2658        assert!(!result.success);
2659        let err = result.error.as_deref().unwrap_or("");
2660        assert!(
2661            !err.contains("sentinel blockhash"),
2662            "EVM TX should pass the sentinel check; got: {err}",
2663        );
2664    }
2665
2666    /// AUDIT-FIX B-1: Treasury lock serializes concurrent fee charging.
2667    /// Two parallel groups charging fees must not lose updates — both debits
2668    /// must be reflected in the final treasury balance.
2669    #[test]
2670    fn test_treasury_lock_prevents_lost_updates() {
2671        let temp_dir = tempdir().unwrap();
2672        let state = StateStore::open(temp_dir.path()).unwrap();
2673        let treasury = Pubkey([3u8; 32]);
2674        state.set_treasury_pubkey(&treasury).unwrap();
2675        state
2676            .put_account(&treasury, &Account::new(0, treasury))
2677            .unwrap();
2678
2679        // Create two payers each with 10 LICN (10_000_000_000 spores)
2680        let kp_a = Keypair::generate();
2681        let kp_b = Keypair::generate();
2682        let payer_a = kp_a.pubkey();
2683        let payer_b = kp_b.pubkey();
2684        let initial_spores = Account::licn_to_spores(10);
2685        state
2686            .put_account(&payer_a, &Account::new(10, payer_a))
2687            .unwrap();
2688        state
2689            .put_account(&payer_b, &Account::new(10, payer_b))
2690            .unwrap();
2691
2692        let fee = Account::licn_to_spores(1); // 1 LICN = 1_000_000_000 spores
2693
2694        // Simulate two parallel groups charging fees concurrently.
2695        // With the treasury_lock, the second group must see the first's write.
2696        let state_a = state.clone();
2697        let state_b = state.clone();
2698
2699        let proc_a = TxProcessor::new(state_a);
2700        let proc_b = TxProcessor::new(state_b);
2701
2702        // Group A charges fee
2703        proc_a.charge_fee_direct(&payer_a, fee).unwrap();
2704
2705        // Group B charges fee — must see group A's treasury credit
2706        proc_b.charge_fee_direct(&payer_b, fee).unwrap();
2707
2708        // Treasury should have received BOTH fee credits (minus burned portion)
2709        let final_treasury = state.get_account(&treasury).unwrap().unwrap();
2710        assert!(
2711            final_treasury.spores > 0,
2712            "Treasury must have received fee credits"
2713        );
2714        // Both payers should have been debited exactly 1 LICN
2715        let payer_a_bal = state.get_account(&payer_a).unwrap().unwrap().spores;
2716        let payer_b_bal = state.get_account(&payer_b).unwrap().unwrap().spores;
2717        assert_eq!(payer_a_bal, initial_spores - fee);
2718        assert_eq!(payer_b_bal, initial_spores - fee);
2719    }
2720
2721    /// AUDIT-FIX B-5: Fee split percentages are capped so total distributed
2722    /// never exceeds the original fee amount.
2723    #[test]
2724    fn test_fee_split_capped_no_spore_creation() {
2725        let (processor, state, _alice_kp, _alice, treasury, _genesis_hash) = setup();
2726
2727        // Set up a payer with known balance (10 LICN)
2728        let payer = Pubkey([99u8; 32]);
2729        state.put_account(&payer, &Account::new(10, payer)).unwrap();
2730
2731        let fee = Account::licn_to_spores(1); // 1 LICN
2732        let treasury_before = state.get_account(&treasury).unwrap().unwrap().spores;
2733
2734        processor.charge_fee_direct(&payer, fee).unwrap();
2735
2736        let treasury_after = state.get_account(&treasury).unwrap().unwrap().spores;
2737        let treasury_gain = treasury_after - treasury_before;
2738        let burned = state.get_total_burned().unwrap_or(0);
2739
2740        // Treasury gain + burned must not exceed the fee charged
2741        assert!(
2742            treasury_gain.saturating_add(burned) <= fee,
2743            "Treasury gain ({}) + burned ({}) must not exceed fee ({})",
2744            treasury_gain,
2745            burned,
2746            fee
2747        );
2748    }
2749
2750    // ====================================================================
2751    // SYSTEM CREATE ACCOUNT (type 1)
2752    // ====================================================================
2753
2754    /// Helper: wrap a single instruction into a signed transaction
2755    fn make_signed_tx(kp: &Keypair, ix: Instruction, recent_blockhash: Hash) -> Transaction {
2756        let message = crate::transaction::Message::new(vec![ix], recent_blockhash);
2757        let mut tx = Transaction::new(message);
2758        let sig = kp.sign(&tx.message.serialize());
2759        tx.signatures.push(sig);
2760        tx
2761    }
2762
2763    #[test]
2764    fn test_create_account_success() {
2765        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
2766        let new_kp = Keypair::generate();
2767        let new_acct = new_kp.pubkey();
2768        let validator = Pubkey([42u8; 32]);
2769
2770        // Two instructions: 1-spore transfer (fee payer = alice), create_account (signer = new_acct)
2771        let message = crate::transaction::Message::new(
2772            vec![
2773                Instruction {
2774                    program_id: SYSTEM_PROGRAM_ID,
2775                    accounts: vec![alice, alice],
2776                    data: {
2777                        let mut d = vec![0u8];
2778                        d.extend_from_slice(&1u64.to_le_bytes());
2779                        d
2780                    },
2781                },
2782                Instruction {
2783                    program_id: SYSTEM_PROGRAM_ID,
2784                    accounts: vec![new_acct],
2785                    data: vec![1],
2786                },
2787            ],
2788            genesis_hash,
2789        );
2790        let mut tx = Transaction::new(message);
2791        let msg_bytes = tx.message.serialize();
2792        tx.signatures.push(alice_kp.sign(&msg_bytes));
2793        tx.signatures.push(new_kp.sign(&msg_bytes));
2794
2795        let result = processor.process_transaction(&tx, &validator);
2796        assert!(
2797            result.success,
2798            "Create account should succeed: {:?}",
2799            result.error
2800        );
2801
2802        let acct = state.get_account(&new_acct).unwrap();
2803        assert!(acct.is_some(), "New account must exist after creation");
2804        assert_eq!(acct.unwrap().spores, 0, "New account should have 0 balance");
2805    }
2806
2807    #[test]
2808    fn test_create_account_already_exists() {
2809        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
2810        let existing_kp = Keypair::generate();
2811        let existing = existing_kp.pubkey();
2812        let validator = Pubkey([42u8; 32]);
2813
2814        // Pre-create the account
2815        state
2816            .put_account(&existing, &Account::new(10, existing))
2817            .unwrap();
2818
2819        let message = crate::transaction::Message::new(
2820            vec![
2821                Instruction {
2822                    program_id: SYSTEM_PROGRAM_ID,
2823                    accounts: vec![alice, alice],
2824                    data: {
2825                        let mut d = vec![0u8];
2826                        d.extend_from_slice(&1u64.to_le_bytes());
2827                        d
2828                    },
2829                },
2830                Instruction {
2831                    program_id: SYSTEM_PROGRAM_ID,
2832                    accounts: vec![existing],
2833                    data: vec![1],
2834                },
2835            ],
2836            genesis_hash,
2837        );
2838        let mut tx = Transaction::new(message);
2839        let msg_bytes = tx.message.serialize();
2840        tx.signatures.push(alice_kp.sign(&msg_bytes));
2841        tx.signatures.push(existing_kp.sign(&msg_bytes));
2842
2843        let result = processor.process_transaction(&tx, &validator);
2844        assert!(!result.success, "Create existing account should fail");
2845        assert!(
2846            result.error.as_ref().unwrap().contains("already exists"),
2847            "Expected 'already exists', got: {:?}",
2848            result.error
2849        );
2850    }
2851
2852    // ====================================================================
2853    // TREASURY TRANSFERS (types 2-5)
2854    // ====================================================================
2855
2856    #[test]
2857    fn test_treasury_transfer_from_treasury_succeeds() {
2858        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
2859        let bob = Pubkey([52u8; 32]);
2860        let validator = Pubkey([42u8; 32]);
2861
2862        // Fund treasury
2863        state
2864            .put_account(&treasury, &Account::new(1_000_000, treasury))
2865            .unwrap();
2866
2867        // Treasury keypair needed to sign
2868        let treasury_kp = Keypair::generate();
2869        let treasury_pub = treasury_kp.pubkey();
2870        state.set_treasury_pubkey(&treasury_pub).unwrap();
2871        let t_acct2 = Account::new(1_000_000, treasury_pub);
2872        state.put_account(&treasury_pub, &t_acct2).unwrap();
2873
2874        let amount = Account::licn_to_spores(100);
2875        let ix = Instruction {
2876            program_id: SYSTEM_PROGRAM_ID,
2877            accounts: vec![treasury_pub, bob],
2878            data: {
2879                let mut d = vec![2u8]; // type 2 = treasury transfer
2880                d.extend_from_slice(&amount.to_le_bytes());
2881                d
2882            },
2883        };
2884        let tx = make_signed_tx(&treasury_kp, ix, genesis_hash);
2885
2886        let result = processor.process_transaction(&tx, &validator);
2887        assert!(
2888            result.success,
2889            "Treasury transfer should succeed: {:?}",
2890            result.error
2891        );
2892        assert_eq!(state.get_balance(&bob).unwrap(), amount);
2893    }
2894
2895    #[test]
2896    fn test_treasury_transfer_from_non_treasury_rejected() {
2897        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
2898        let bob = Pubkey([53u8; 32]);
2899        let validator = Pubkey([42u8; 32]);
2900
2901        let amount = Account::licn_to_spores(10);
2902        let ix = Instruction {
2903            program_id: SYSTEM_PROGRAM_ID,
2904            accounts: vec![alice, bob],
2905            data: {
2906                let mut d = vec![3u8]; // type 3 = treasury transfer
2907                d.extend_from_slice(&amount.to_le_bytes());
2908                d
2909            },
2910        };
2911        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
2912
2913        let result = processor.process_transaction(&tx, &validator);
2914        assert!(!result.success, "Non-treasury should not use types 2-5");
2915        assert!(result.error.unwrap().contains("restricted to treasury"));
2916    }
2917
2918    // ====================================================================
2919    // NFT OPERATIONS (types 6, 7, 8)
2920    // ====================================================================
2921
2922    /// Helper: create a collection and return the collection account pubkey.
2923    /// NOTE: Funds the creator with extra LICN to cover the 1000 LICN collection fee.
2924    fn create_test_collection(
2925        processor: &TxProcessor,
2926        state: &StateStore,
2927        creator_kp: &Keypair,
2928        creator: Pubkey,
2929        collection_addr: Pubkey,
2930        genesis_hash: Hash,
2931    ) -> TxResult {
2932        // Ensure creator has enough for the collection fee (1000 LICN) + base fee
2933        state
2934            .put_account(&creator, &Account::new(10_000, creator))
2935            .unwrap();
2936        let col_data = crate::nft::CreateCollectionData {
2937            name: "TestCollection".to_string(),
2938            symbol: "TNFT".to_string(),
2939            royalty_bps: 500,
2940            max_supply: 100,
2941            public_mint: true,
2942            mint_authority: None,
2943        };
2944        let encoded = bincode::serialize(&col_data).unwrap();
2945        let mut data = vec![6u8];
2946        data.extend_from_slice(&encoded);
2947
2948        let ix = Instruction {
2949            program_id: SYSTEM_PROGRAM_ID,
2950            accounts: vec![creator, collection_addr],
2951            data,
2952        };
2953        let tx = make_signed_tx(creator_kp, ix, genesis_hash);
2954        processor.process_transaction(&tx, &Pubkey([42u8; 32]))
2955    }
2956
2957    #[test]
2958    fn test_create_collection_success() {
2959        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
2960        let collection = Pubkey([60u8; 32]);
2961
2962        let result = create_test_collection(
2963            &processor,
2964            &state,
2965            &alice_kp,
2966            alice,
2967            collection,
2968            genesis_hash,
2969        );
2970        assert!(
2971            result.success,
2972            "Collection creation should succeed: {:?}",
2973            result.error
2974        );
2975
2976        let acct = state.get_account(&collection).unwrap().unwrap();
2977        let col_state = crate::nft::decode_collection_state(&acct.data).unwrap();
2978        assert_eq!(col_state.name, "TestCollection");
2979        assert_eq!(col_state.symbol, "TNFT");
2980        assert_eq!(col_state.creator, alice);
2981        assert_eq!(col_state.max_supply, 100);
2982        assert_eq!(col_state.minted, 0);
2983    }
2984
2985    #[test]
2986    fn test_create_collection_duplicate_rejected() {
2987        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
2988        let collection = Pubkey([61u8; 32]);
2989
2990        // First creation succeeds
2991        let r1 = create_test_collection(
2992            &processor,
2993            &state,
2994            &alice_kp,
2995            alice,
2996            collection,
2997            genesis_hash,
2998        );
2999        assert!(r1.success, "First creation should succeed: {:?}", r1.error);
3000
3001        // Ensure alice has balance for the second attempt
3002        state
3003            .put_account(&alice, &Account::new(10_000, alice))
3004            .unwrap();
3005
3006        // Try to create again with slightly different data to avoid replay protection
3007        let col_data = crate::nft::CreateCollectionData {
3008            name: "TestCollection2".to_string(),
3009            symbol: "TNFT".to_string(),
3010            royalty_bps: 500,
3011            max_supply: 100,
3012            public_mint: true,
3013            mint_authority: None,
3014        };
3015        let encoded = bincode::serialize(&col_data).unwrap();
3016        let mut data = vec![6u8];
3017        data.extend_from_slice(&encoded);
3018        let ix = Instruction {
3019            program_id: SYSTEM_PROGRAM_ID,
3020            accounts: vec![alice, collection],
3021            data,
3022        };
3023        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
3024        let r2 = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
3025        assert!(!r2.success, "Duplicate collection should fail");
3026        assert!(
3027            r2.error.as_ref().unwrap().contains("already exists"),
3028            "Expected 'already exists', got: {:?}",
3029            r2.error
3030        );
3031    }
3032
3033    #[test]
3034    fn test_mint_nft_success() {
3035        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3036        let collection = Pubkey([62u8; 32]);
3037        let token_addr = Pubkey([63u8; 32]);
3038
3039        // Create collection first
3040        let r = create_test_collection(
3041            &processor,
3042            &state,
3043            &alice_kp,
3044            alice,
3045            collection,
3046            genesis_hash,
3047        );
3048        assert!(
3049            r.success,
3050            "Setup: collection creation failed: {:?}",
3051            r.error
3052        );
3053
3054        // Mint NFT
3055        let mint_data = crate::nft::MintNftData {
3056            token_id: 1,
3057            metadata_uri: "https://example.com/nft/1.json".to_string(),
3058        };
3059        let encoded = bincode::serialize(&mint_data).unwrap();
3060        let mut data = vec![7u8];
3061        data.extend_from_slice(&encoded);
3062
3063        let ix = Instruction {
3064            program_id: SYSTEM_PROGRAM_ID,
3065            accounts: vec![alice, collection, token_addr, alice], // minter, collection, token, owner
3066            data,
3067        };
3068        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
3069        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
3070        assert!(result.success, "Mint should succeed: {:?}", result.error);
3071
3072        // Verify token state
3073        let token_acct = state.get_account(&token_addr).unwrap().unwrap();
3074        let token_state = crate::nft::decode_token_state(&token_acct.data).unwrap();
3075        assert_eq!(token_state.owner, alice);
3076        assert_eq!(token_state.collection, collection);
3077        assert_eq!(token_state.token_id, 1);
3078
3079        // Verify collection minted count incremented
3080        let col_acct = state.get_account(&collection).unwrap().unwrap();
3081        let col_state = crate::nft::decode_collection_state(&col_acct.data).unwrap();
3082        assert_eq!(col_state.minted, 1);
3083    }
3084
3085    #[test]
3086    fn test_mint_nft_duplicate_token_id_rejected() {
3087        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3088        let collection = Pubkey([64u8; 32]);
3089        let token1 = Pubkey([65u8; 32]);
3090        let token2 = Pubkey([66u8; 32]);
3091
3092        // Create collection + mint token_id=1
3093        create_test_collection(
3094            &processor,
3095            &state,
3096            &alice_kp,
3097            alice,
3098            collection,
3099            genesis_hash,
3100        );
3101        let mint_data = crate::nft::MintNftData {
3102            token_id: 1,
3103            metadata_uri: "https://example.com/1.json".to_string(),
3104        };
3105        let encoded = bincode::serialize(&mint_data).unwrap();
3106        let mut data = vec![7u8];
3107        data.extend_from_slice(&encoded);
3108
3109        let ix1 = Instruction {
3110            program_id: SYSTEM_PROGRAM_ID,
3111            accounts: vec![alice, collection, token1, alice],
3112            data: data.clone(),
3113        };
3114        let tx1 = make_signed_tx(&alice_kp, ix1, genesis_hash);
3115        let r1 = processor.process_transaction(&tx1, &Pubkey([42u8; 32]));
3116        assert!(r1.success, "First mint should succeed");
3117
3118        // Mint with same token_id=1 but different token address
3119        let ix2 = Instruction {
3120            program_id: SYSTEM_PROGRAM_ID,
3121            accounts: vec![alice, collection, token2, alice],
3122            data,
3123        };
3124        let tx2 = make_signed_tx(&alice_kp, ix2, genesis_hash);
3125        let r2 = processor.process_transaction(&tx2, &Pubkey([42u8; 32]));
3126        assert!(!r2.success, "Duplicate token_id should fail");
3127        assert!(r2.error.unwrap().contains("already exists"));
3128    }
3129
3130    #[test]
3131    fn test_transfer_nft_success() {
3132        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3133        let bob = Pubkey([67u8; 32]);
3134        let collection = Pubkey([68u8; 32]);
3135        let token_addr = Pubkey([69u8; 32]);
3136
3137        // Create collection + mint
3138        create_test_collection(
3139            &processor,
3140            &state,
3141            &alice_kp,
3142            alice,
3143            collection,
3144            genesis_hash,
3145        );
3146        let mint_data = crate::nft::MintNftData {
3147            token_id: 1,
3148            metadata_uri: "https://example.com/1.json".to_string(),
3149        };
3150        let mut mdata = vec![7u8];
3151        mdata.extend_from_slice(&bincode::serialize(&mint_data).unwrap());
3152        let ix_mint = Instruction {
3153            program_id: SYSTEM_PROGRAM_ID,
3154            accounts: vec![alice, collection, token_addr, alice],
3155            data: mdata,
3156        };
3157        let tx_mint = make_signed_tx(&alice_kp, ix_mint, genesis_hash);
3158        let r = processor.process_transaction(&tx_mint, &Pubkey([42u8; 32]));
3159        assert!(r.success, "Mint failed: {:?}", r.error);
3160
3161        // Transfer NFT from alice to bob
3162        let ix_transfer = Instruction {
3163            program_id: SYSTEM_PROGRAM_ID,
3164            accounts: vec![alice, token_addr, bob],
3165            data: vec![8u8],
3166        };
3167        let tx_transfer = make_signed_tx(&alice_kp, ix_transfer, genesis_hash);
3168        let result = processor.process_transaction(&tx_transfer, &Pubkey([42u8; 32]));
3169        assert!(
3170            result.success,
3171            "NFT transfer should succeed: {:?}",
3172            result.error
3173        );
3174
3175        let token_acct = state.get_account(&token_addr).unwrap().unwrap();
3176        let token_state = crate::nft::decode_token_state(&token_acct.data).unwrap();
3177        assert_eq!(token_state.owner, bob, "Owner should be bob after transfer");
3178    }
3179
3180    #[test]
3181    fn test_transfer_nft_unauthorized_rejected() {
3182        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3183        let collection = Pubkey([70u8; 32]);
3184        let token_addr = Pubkey([71u8; 32]);
3185        let bob = Pubkey([72u8; 32]);
3186        let eve_kp = Keypair::generate();
3187        let eve = eve_kp.pubkey();
3188        state.put_account(&eve, &Account::new(100, eve)).unwrap();
3189
3190        // Create + mint (alice owns)
3191        create_test_collection(
3192            &processor,
3193            &state,
3194            &alice_kp,
3195            alice,
3196            collection,
3197            genesis_hash,
3198        );
3199        let mint_data = crate::nft::MintNftData {
3200            token_id: 1,
3201            metadata_uri: "uri".to_string(),
3202        };
3203        let mut mdata = vec![7u8];
3204        mdata.extend_from_slice(&bincode::serialize(&mint_data).unwrap());
3205        let ix = Instruction {
3206            program_id: SYSTEM_PROGRAM_ID,
3207            accounts: vec![alice, collection, token_addr, alice],
3208            data: mdata,
3209        };
3210        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
3211        let r = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
3212        assert!(r.success, "Mint should succeed: {:?}", r.error);
3213
3214        // Eve tries to transfer alice's NFT
3215        let ix_transfer = Instruction {
3216            program_id: SYSTEM_PROGRAM_ID,
3217            accounts: vec![eve, token_addr, bob],
3218            data: vec![8u8],
3219        };
3220        let tx_transfer = make_signed_tx(&eve_kp, ix_transfer, genesis_hash);
3221        let result = processor.process_transaction(&tx_transfer, &Pubkey([42u8; 32]));
3222        assert!(!result.success, "Eve should not transfer alice's NFT");
3223        assert!(
3224            result.error.as_ref().unwrap().contains("Unauthorized"),
3225            "Expected 'Unauthorized', got: {:?}",
3226            result.error
3227        );
3228    }
3229
3230    // ====================================================================
3231    // STAKING OPERATIONS (types 9, 10, 11)
3232    // ====================================================================
3233
3234    /// Helper: set up a validator in the stake pool so staking tests can run
3235    fn setup_validator_in_pool(state: &StateStore, validator: Pubkey) {
3236        let mut pool = state.get_stake_pool().unwrap_or_default();
3237        // Insert validator with MIN_VALIDATOR_STAKE so the validator entry exists
3238        pool.upsert_stake(validator, crate::consensus::MIN_VALIDATOR_STAKE, 0);
3239        state.put_stake_pool(&pool).unwrap();
3240    }
3241
3242    fn make_stake_tx(
3243        kp: &Keypair,
3244        staker: Pubkey,
3245        validator: Pubkey,
3246        amount: u64,
3247        recent_blockhash: Hash,
3248    ) -> Transaction {
3249        let ix = Instruction {
3250            program_id: SYSTEM_PROGRAM_ID,
3251            accounts: vec![staker, validator],
3252            data: {
3253                let mut d = vec![9u8];
3254                d.extend_from_slice(&amount.to_le_bytes());
3255                d
3256            },
3257        };
3258        make_signed_tx(kp, ix, recent_blockhash)
3259    }
3260
3261    fn make_request_unstake_tx(
3262        kp: &Keypair,
3263        staker: Pubkey,
3264        validator: Pubkey,
3265        amount: u64,
3266        recent_blockhash: Hash,
3267    ) -> Transaction {
3268        let ix = Instruction {
3269            program_id: SYSTEM_PROGRAM_ID,
3270            accounts: vec![staker, validator],
3271            data: {
3272                let mut d = vec![10u8];
3273                d.extend_from_slice(&amount.to_le_bytes());
3274                d
3275            },
3276        };
3277        make_signed_tx(kp, ix, recent_blockhash)
3278    }
3279
3280    fn make_claim_unstake_tx(
3281        kp: &Keypair,
3282        staker: Pubkey,
3283        validator: Pubkey,
3284        recent_blockhash: Hash,
3285    ) -> Transaction {
3286        let ix = Instruction {
3287            program_id: SYSTEM_PROGRAM_ID,
3288            accounts: vec![staker, validator],
3289            data: vec![11u8],
3290        };
3291        make_signed_tx(kp, ix, recent_blockhash)
3292    }
3293
3294    fn make_register_validator_tx(
3295        kp: &Keypair,
3296        validator: Pubkey,
3297        fingerprint: [u8; 32],
3298        recent_blockhash: Hash,
3299    ) -> Transaction {
3300        let mut data = vec![26u8];
3301        data.extend_from_slice(&fingerprint);
3302        let ix = Instruction {
3303            program_id: SYSTEM_PROGRAM_ID,
3304            accounts: vec![validator],
3305            data,
3306        };
3307        make_signed_tx(kp, ix, recent_blockhash)
3308    }
3309
3310    fn make_deregister_validator_tx(
3311        kp: &Keypair,
3312        validator: Pubkey,
3313        recent_blockhash: Hash,
3314    ) -> Transaction {
3315        let ix = Instruction {
3316            program_id: SYSTEM_PROGRAM_ID,
3317            accounts: vec![validator],
3318            data: vec![31u8],
3319        };
3320        make_signed_tx(kp, ix, recent_blockhash)
3321    }
3322
3323    fn fund_treasury_for_validator_bootstrap(state: &StateStore, treasury: Pubkey) {
3324        state
3325            .put_account(&treasury, &Account::new(500_000, treasury))
3326            .unwrap();
3327    }
3328
3329    fn assert_validator_registration_not_granted(
3330        state: &StateStore,
3331        treasury: Pubkey,
3332        before_treasury: &Account,
3333        validator: Pubkey,
3334        fingerprint: [u8; 32],
3335    ) {
3336        let after_treasury = state.get_account(&treasury).unwrap().unwrap();
3337        assert_eq!(after_treasury.spores, before_treasury.spores);
3338        assert_eq!(after_treasury.spendable, before_treasury.spendable);
3339        assert!(state.get_account(&validator).unwrap().is_none());
3340        let pool = state.get_stake_pool().unwrap();
3341        assert_eq!(pool.bootstrap_grants_issued(), 0);
3342        assert!(pool.get_stake(&validator).is_none());
3343        assert!(pool.fingerprint_owner(&fingerprint).is_none());
3344    }
3345
3346    #[test]
3347    fn test_stake_success() {
3348        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3349        let validator = Pubkey([42u8; 32]);
3350
3351        // Register validator in pool
3352        setup_validator_in_pool(&state, validator);
3353
3354        // Fund alice with enough for MIN_VALIDATOR_STAKE (75K LICN)
3355        state
3356            .put_account(&alice, &Account::new(100_000, alice))
3357            .unwrap();
3358
3359        // Stake at MIN_VALIDATOR_STAKE
3360        let amount = crate::consensus::MIN_VALIDATOR_STAKE;
3361        let ix = Instruction {
3362            program_id: SYSTEM_PROGRAM_ID,
3363            accounts: vec![alice, validator],
3364            data: {
3365                let mut d = vec![9u8];
3366                d.extend_from_slice(&amount.to_le_bytes());
3367                d
3368            },
3369        };
3370        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
3371        let result = processor.process_transaction(&tx, &validator);
3372        assert!(result.success, "Staking should succeed: {:?}", result.error);
3373
3374        // Verify alice's staked balance
3375        let acct = state.get_account(&alice).unwrap().unwrap();
3376        assert_eq!(
3377            acct.staked, amount,
3378            "Staked balance should equal MIN_VALIDATOR_STAKE"
3379        );
3380
3381        // Verify stake pool updated
3382        let pool = state.get_stake_pool().unwrap();
3383        let stake_info = pool.get_stake(&validator).unwrap();
3384        assert!(
3385            stake_info.amount >= amount,
3386            "Stake pool should reflect the staked amount"
3387        );
3388    }
3389
3390    #[test]
3391    fn test_stake_to_unregistered_validator_rejected() {
3392        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
3393        let fake_validator = Pubkey([99u8; 32]); // Not in stake pool
3394
3395        let amount = Account::licn_to_spores(100);
3396        let ix = Instruction {
3397            program_id: SYSTEM_PROGRAM_ID,
3398            accounts: vec![alice, fake_validator],
3399            data: {
3400                let mut d = vec![9u8];
3401                d.extend_from_slice(&amount.to_le_bytes());
3402                d
3403            },
3404        };
3405        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
3406        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
3407        assert!(
3408            !result.success,
3409            "Staking to unregistered validator should fail"
3410        );
3411        assert!(result.error.unwrap().contains("not registered"));
3412    }
3413
3414    #[test]
3415    fn test_stake_rejects_outgoing_restricted_staker_without_pool_mutation() {
3416        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3417        let validator = Pubkey([42u8; 32]);
3418        setup_validator_in_pool(&state, validator);
3419        state
3420            .put_account(&alice, &Account::new(100_000, alice))
3421            .unwrap();
3422        let before_pool_stake = state
3423            .get_stake_pool()
3424            .unwrap()
3425            .get_stake(&validator)
3426            .unwrap()
3427            .amount;
3428
3429        put_active_processor_test_restriction(
3430            &state,
3431            RestrictionTarget::Account(alice),
3432            RestrictionMode::OutgoingOnly,
3433        );
3434
3435        let tx = make_stake_tx(
3436            &alice_kp,
3437            alice,
3438            validator,
3439            crate::consensus::MIN_VALIDATOR_STAKE,
3440            genesis_hash,
3441        );
3442        let result = processor.process_transaction(&tx, &validator);
3443        assert!(!result.success);
3444        assert!(result
3445            .error
3446            .as_deref()
3447            .unwrap_or("")
3448            .contains("Stake blocked by active staker account restriction"));
3449
3450        let after_account = state.get_account(&alice).unwrap().unwrap();
3451        assert_eq!(after_account.staked, 0);
3452        assert_eq!(after_account.locked, 0);
3453        let after_pool_stake = state
3454            .get_stake_pool()
3455            .unwrap()
3456            .get_stake(&validator)
3457            .unwrap()
3458            .amount;
3459        assert_eq!(after_pool_stake, before_pool_stake);
3460    }
3461
3462    #[test]
3463    fn test_stake_rejects_native_frozen_amount_without_pool_mutation() {
3464        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3465        let validator = Pubkey([42u8; 32]);
3466        setup_validator_in_pool(&state, validator);
3467        state
3468            .put_account(&alice, &Account::new(100_000, alice))
3469            .unwrap();
3470        let before_pool_stake = state
3471            .get_stake_pool()
3472            .unwrap()
3473            .get_stake(&validator)
3474            .unwrap()
3475            .amount;
3476        let frozen_amount = state.get_account(&alice).unwrap().unwrap().spendable;
3477
3478        put_active_processor_test_restriction(
3479            &state,
3480            RestrictionTarget::AccountAsset {
3481                account: alice,
3482                asset: NATIVE_LICN_ASSET_ID,
3483            },
3484            RestrictionMode::FrozenAmount {
3485                amount: frozen_amount,
3486            },
3487        );
3488
3489        let tx = make_stake_tx(
3490            &alice_kp,
3491            alice,
3492            validator,
3493            crate::consensus::MIN_VALIDATOR_STAKE,
3494            genesis_hash,
3495        );
3496        let result = processor.process_transaction(&tx, &validator);
3497        assert!(!result.success);
3498        assert!(result
3499            .error
3500            .as_deref()
3501            .unwrap_or("")
3502            .contains("Stake blocked by active staker native account-asset restriction"));
3503
3504        let after_account = state.get_account(&alice).unwrap().unwrap();
3505        assert_eq!(after_account.staked, 0);
3506        assert_eq!(
3507            state
3508                .get_stake_pool()
3509                .unwrap()
3510                .get_stake(&validator)
3511                .unwrap()
3512                .amount,
3513            before_pool_stake
3514        );
3515    }
3516
3517    #[test]
3518    fn test_staking_protocol_pause_rejects_stake_without_pool_mutation() {
3519        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3520        let validator = Pubkey([42u8; 32]);
3521        setup_validator_in_pool(&state, validator);
3522        state
3523            .put_account(&alice, &Account::new(100_000, alice))
3524            .unwrap();
3525        let before_pool_stake = state
3526            .get_stake_pool()
3527            .unwrap()
3528            .get_stake(&validator)
3529            .unwrap()
3530            .amount;
3531
3532        put_active_processor_test_restriction(
3533            &state,
3534            RestrictionTarget::ProtocolModule(ProtocolModuleId::Staking),
3535            RestrictionMode::ProtocolPaused,
3536        );
3537
3538        let tx = make_stake_tx(
3539            &alice_kp,
3540            alice,
3541            validator,
3542            crate::consensus::MIN_VALIDATOR_STAKE,
3543            genesis_hash,
3544        );
3545        let result = processor.process_transaction(&tx, &validator);
3546        assert!(!result.success);
3547        assert!(result
3548            .error
3549            .as_deref()
3550            .unwrap_or("")
3551            .contains("Stake blocked by active Staking protocol pause"));
3552
3553        let after_account = state.get_account(&alice).unwrap().unwrap();
3554        assert_eq!(after_account.staked, 0);
3555        assert_eq!(
3556            state
3557                .get_stake_pool()
3558                .unwrap()
3559                .get_stake(&validator)
3560                .unwrap()
3561                .amount,
3562            before_pool_stake
3563        );
3564    }
3565
3566    #[test]
3567    fn test_request_unstake_success() {
3568        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3569        let validator = Pubkey([42u8; 32]);
3570
3571        setup_validator_in_pool(&state, validator);
3572
3573        // Fund alice
3574        state
3575            .put_account(&alice, &Account::new(100_000, alice))
3576            .unwrap();
3577
3578        // Stake MIN_VALIDATOR_STAKE first
3579        let amount = crate::consensus::MIN_VALIDATOR_STAKE;
3580        let ix_stake = Instruction {
3581            program_id: SYSTEM_PROGRAM_ID,
3582            accounts: vec![alice, validator],
3583            data: {
3584                let mut d = vec![9u8];
3585                d.extend_from_slice(&amount.to_le_bytes());
3586                d
3587            },
3588        };
3589        let tx_stake = make_signed_tx(&alice_kp, ix_stake, genesis_hash);
3590        let r = processor.process_transaction(&tx_stake, &validator);
3591        assert!(r.success, "Stake should succeed: {:?}", r.error);
3592
3593        // Request unstake — partial amount to avoid going below minimum
3594        let unstake_amount = amount / 2;
3595        let ix_unstake = Instruction {
3596            program_id: SYSTEM_PROGRAM_ID,
3597            accounts: vec![alice, validator],
3598            data: {
3599                let mut d = vec![10u8];
3600                d.extend_from_slice(&unstake_amount.to_le_bytes());
3601                d
3602            },
3603        };
3604        let tx_unstake = make_signed_tx(&alice_kp, ix_unstake, genesis_hash);
3605        let result = processor.process_transaction(&tx_unstake, &validator);
3606        assert!(result.success, "Unstake should succeed: {:?}", result.error);
3607
3608        // Verify staked balance decreased and locked increased
3609        let acct = state.get_account(&alice).unwrap().unwrap();
3610        assert_eq!(
3611            acct.staked,
3612            amount - unstake_amount,
3613            "Staked should be reduced"
3614        );
3615        assert_eq!(
3616            acct.locked, unstake_amount,
3617            "Locked should equal unstaked amount"
3618        );
3619    }
3620
3621    #[test]
3622    fn test_request_unstake_rejects_outgoing_restricted_staker_without_request() {
3623        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3624        let validator = Pubkey([42u8; 32]);
3625        setup_validator_in_pool(&state, validator);
3626        state
3627            .put_account(&alice, &Account::new(100_000, alice))
3628            .unwrap();
3629
3630        let amount = crate::consensus::MIN_VALIDATOR_STAKE;
3631        let stake_tx = make_stake_tx(&alice_kp, alice, validator, amount, genesis_hash);
3632        assert!(processor.process_transaction(&stake_tx, &validator).success);
3633        let before_account = state.get_account(&alice).unwrap().unwrap();
3634        let before_pool = state.get_stake_pool().unwrap();
3635        assert!(before_pool.get_unstake_request(&validator).is_none());
3636
3637        put_active_processor_test_restriction(
3638            &state,
3639            RestrictionTarget::Account(alice),
3640            RestrictionMode::OutgoingOnly,
3641        );
3642
3643        let unstake_amount = amount / 2;
3644        let tx = make_request_unstake_tx(&alice_kp, alice, validator, unstake_amount, genesis_hash);
3645        let result = processor.process_transaction(&tx, &validator);
3646        assert!(!result.success);
3647        assert!(result
3648            .error
3649            .as_deref()
3650            .unwrap_or("")
3651            .contains("RequestUnstake blocked by active staker account restriction"));
3652
3653        let after_account = state.get_account(&alice).unwrap().unwrap();
3654        assert_eq!(after_account.staked, before_account.staked);
3655        assert_eq!(after_account.locked, before_account.locked);
3656        let after_pool = state.get_stake_pool().unwrap();
3657        assert!(after_pool.get_unstake_request(&validator).is_none());
3658        assert_eq!(
3659            after_pool.get_stake(&validator).unwrap().amount,
3660            before_pool.get_stake(&validator).unwrap().amount
3661        );
3662    }
3663
3664    #[test]
3665    fn test_request_unstake_insufficient_rejected() {
3666        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3667        let validator = Pubkey([42u8; 32]);
3668
3669        setup_validator_in_pool(&state, validator);
3670
3671        // Fund alice
3672        state
3673            .put_account(&alice, &Account::new(100_000, alice))
3674            .unwrap();
3675
3676        // Stake MIN_VALIDATOR_STAKE
3677        let stake_amount = crate::consensus::MIN_VALIDATOR_STAKE;
3678        let ix_stake = Instruction {
3679            program_id: SYSTEM_PROGRAM_ID,
3680            accounts: vec![alice, validator],
3681            data: {
3682                let mut d = vec![9u8];
3683                d.extend_from_slice(&stake_amount.to_le_bytes());
3684                d
3685            },
3686        };
3687        let tx = make_signed_tx(&alice_kp, ix_stake, genesis_hash);
3688        let r = processor.process_transaction(&tx, &validator);
3689        assert!(r.success, "Stake should succeed: {:?}", r.error);
3690
3691        // Try to unstake more than staked
3692        let too_much = Account::licn_to_spores(100_000);
3693        let ix_unstake = Instruction {
3694            program_id: SYSTEM_PROGRAM_ID,
3695            accounts: vec![alice, validator],
3696            data: {
3697                let mut d = vec![10u8];
3698                d.extend_from_slice(&too_much.to_le_bytes());
3699                d
3700            },
3701        };
3702        let tx2 = make_signed_tx(&alice_kp, ix_unstake, genesis_hash);
3703        let result = processor.process_transaction(&tx2, &validator);
3704        assert!(!result.success, "Unstaking more than staked should fail");
3705        assert!(
3706            result.error.as_ref().unwrap().contains("Insufficient"),
3707            "Expected 'Insufficient', got: {:?}",
3708            result.error
3709        );
3710    }
3711
3712    #[test]
3713    fn test_claim_unstake_before_cooldown_rejected() {
3714        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3715        let validator = Pubkey([42u8; 32]);
3716
3717        setup_validator_in_pool(&state, validator);
3718
3719        // Fund alice
3720        state
3721            .put_account(&alice, &Account::new(200_000, alice))
3722            .unwrap();
3723
3724        // Stake MIN_VALIDATOR_STAKE
3725        let amount = crate::consensus::MIN_VALIDATOR_STAKE;
3726        let ix_s = Instruction {
3727            program_id: SYSTEM_PROGRAM_ID,
3728            accounts: vec![alice, validator],
3729            data: {
3730                let mut d = vec![9u8];
3731                d.extend_from_slice(&amount.to_le_bytes());
3732                d
3733            },
3734        };
3735        let r = processor
3736            .process_transaction(&make_signed_tx(&alice_kp, ix_s, genesis_hash), &validator);
3737        assert!(r.success, "Stake failed: {:?}", r.error);
3738
3739        // Request unstake — half
3740        let unstake_amount = amount / 2;
3741        let ix_u = Instruction {
3742            program_id: SYSTEM_PROGRAM_ID,
3743            accounts: vec![alice, validator],
3744            data: {
3745                let mut d = vec![10u8];
3746                d.extend_from_slice(&unstake_amount.to_le_bytes());
3747                d
3748            },
3749        };
3750        let r2 = processor
3751            .process_transaction(&make_signed_tx(&alice_kp, ix_u, genesis_hash), &validator);
3752        assert!(r2.success, "Unstake request failed: {:?}", r2.error);
3753
3754        // Immediately try to claim (cooldown not passed — slot is still 0)
3755        let ix_claim = Instruction {
3756            program_id: SYSTEM_PROGRAM_ID,
3757            accounts: vec![alice, validator],
3758            data: vec![11u8],
3759        };
3760        let tx_claim = make_signed_tx(&alice_kp, ix_claim, genesis_hash);
3761        let result = processor.process_transaction(&tx_claim, &validator);
3762        assert!(!result.success, "Claim before cooldown should fail");
3763    }
3764
3765    #[test]
3766    fn test_claim_unstake_rejects_incoming_restricted_staker_without_unlocking() {
3767        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3768        let validator = Pubkey([42u8; 32]);
3769        setup_validator_in_pool(&state, validator);
3770        state
3771            .put_account(&alice, &Account::new(200_000, alice))
3772            .unwrap();
3773
3774        let amount = crate::consensus::MIN_VALIDATOR_STAKE;
3775        let stake_tx = make_stake_tx(&alice_kp, alice, validator, amount, genesis_hash);
3776        assert!(processor.process_transaction(&stake_tx, &validator).success);
3777        let unstake_amount = amount / 2;
3778        let unstake_tx =
3779            make_request_unstake_tx(&alice_kp, alice, validator, unstake_amount, genesis_hash);
3780        assert!(
3781            processor
3782                .process_transaction(&unstake_tx, &validator)
3783                .success
3784        );
3785        let before_account = state.get_account(&alice).unwrap().unwrap();
3786        assert_eq!(before_account.locked, unstake_amount);
3787        assert!(state
3788            .get_stake_pool()
3789            .unwrap()
3790            .get_unstake_request(&validator)
3791            .is_some());
3792
3793        put_active_processor_test_restriction(
3794            &state,
3795            RestrictionTarget::Account(alice),
3796            RestrictionMode::IncomingOnly,
3797        );
3798        let future_hash = advance_test_slot(&state, crate::consensus::UNSTAKE_COOLDOWN_SLOTS + 1);
3799        let claim_tx = make_claim_unstake_tx(&alice_kp, alice, validator, future_hash);
3800        let result = processor.process_transaction(&claim_tx, &validator);
3801        assert!(!result.success);
3802        assert!(result
3803            .error
3804            .as_deref()
3805            .unwrap_or("")
3806            .contains("ClaimUnstake blocked by active staker account restriction"));
3807
3808        let after_account = state.get_account(&alice).unwrap().unwrap();
3809        assert_eq!(after_account.staked, before_account.staked);
3810        assert_eq!(after_account.locked, before_account.locked);
3811        assert!(state
3812            .get_stake_pool()
3813            .unwrap()
3814            .get_unstake_request(&validator)
3815            .is_some());
3816    }
3817
3818    #[test]
3819    fn test_register_validator_rejects_treasury_outgoing_restriction_without_grant() {
3820        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
3821        let block_producer = Pubkey([42u8; 32]);
3822        fund_treasury_for_validator_bootstrap(&state, treasury);
3823        let before_treasury = state.get_account(&treasury).unwrap().unwrap();
3824        let validator_kp = Keypair::generate();
3825        let validator = validator_kp.pubkey();
3826        let fingerprint = [0x31; 32];
3827
3828        put_active_processor_test_restriction(
3829            &state,
3830            RestrictionTarget::Account(treasury),
3831            RestrictionMode::OutgoingOnly,
3832        );
3833
3834        let tx = make_register_validator_tx(&validator_kp, validator, fingerprint, genesis_hash);
3835        let result = processor.process_transaction(&tx, &block_producer);
3836        assert!(!result.success);
3837        assert!(result
3838            .error
3839            .as_deref()
3840            .unwrap_or("")
3841            .contains("RegisterValidator blocked by active treasury account restriction"));
3842        assert_validator_registration_not_granted(
3843            &state,
3844            treasury,
3845            &before_treasury,
3846            validator,
3847            fingerprint,
3848        );
3849    }
3850
3851    #[test]
3852    fn test_register_validator_rejects_treasury_native_frozen_amount_without_grant() {
3853        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
3854        let block_producer = Pubkey([42u8; 32]);
3855        fund_treasury_for_validator_bootstrap(&state, treasury);
3856        let before_treasury = state.get_account(&treasury).unwrap().unwrap();
3857        let validator_kp = Keypair::generate();
3858        let validator = validator_kp.pubkey();
3859        let fingerprint = [0x32; 32];
3860
3861        put_active_processor_test_restriction(
3862            &state,
3863            RestrictionTarget::AccountAsset {
3864                account: treasury,
3865                asset: NATIVE_LICN_ASSET_ID,
3866            },
3867            RestrictionMode::FrozenAmount {
3868                amount: before_treasury.spendable,
3869            },
3870        );
3871
3872        let tx = make_register_validator_tx(&validator_kp, validator, fingerprint, genesis_hash);
3873        let result = processor.process_transaction(&tx, &block_producer);
3874        assert!(!result.success);
3875        assert!(result.error.as_deref().unwrap_or("").contains(
3876            "RegisterValidator blocked by active treasury native account-asset restriction"
3877        ));
3878        assert_validator_registration_not_granted(
3879            &state,
3880            treasury,
3881            &before_treasury,
3882            validator,
3883            fingerprint,
3884        );
3885    }
3886
3887    #[test]
3888    fn test_register_validator_rejects_incoming_restricted_validator_without_grant() {
3889        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
3890        let block_producer = Pubkey([42u8; 32]);
3891        fund_treasury_for_validator_bootstrap(&state, treasury);
3892        let before_treasury = state.get_account(&treasury).unwrap().unwrap();
3893        let validator_kp = Keypair::generate();
3894        let validator = validator_kp.pubkey();
3895        let fingerprint = [0x33; 32];
3896
3897        put_active_processor_test_restriction(
3898            &state,
3899            RestrictionTarget::Account(validator),
3900            RestrictionMode::IncomingOnly,
3901        );
3902
3903        let tx = make_register_validator_tx(&validator_kp, validator, fingerprint, genesis_hash);
3904        let result = processor.process_transaction(&tx, &block_producer);
3905        assert!(!result.success);
3906        assert!(result
3907            .error
3908            .as_deref()
3909            .unwrap_or("")
3910            .contains("RegisterValidator blocked by active validator account restriction"));
3911        assert_validator_registration_not_granted(
3912            &state,
3913            treasury,
3914            &before_treasury,
3915            validator,
3916            fingerprint,
3917        );
3918    }
3919
3920    #[test]
3921    fn test_register_validator_protocol_pause_rejects_without_grant() {
3922        let (processor, state, _alice_kp, _alice, treasury, genesis_hash) = setup();
3923        let block_producer = Pubkey([42u8; 32]);
3924        fund_treasury_for_validator_bootstrap(&state, treasury);
3925        let before_treasury = state.get_account(&treasury).unwrap().unwrap();
3926        let validator_kp = Keypair::generate();
3927        let validator = validator_kp.pubkey();
3928        let fingerprint = [0x34; 32];
3929
3930        put_active_processor_test_restriction(
3931            &state,
3932            RestrictionTarget::ProtocolModule(ProtocolModuleId::Staking),
3933            RestrictionMode::ProtocolPaused,
3934        );
3935
3936        let tx = make_register_validator_tx(&validator_kp, validator, fingerprint, genesis_hash);
3937        let result = processor.process_transaction(&tx, &block_producer);
3938        assert!(!result.success);
3939        assert!(result
3940            .error
3941            .as_deref()
3942            .unwrap_or("")
3943            .contains("RegisterValidator blocked by active Staking protocol pause"));
3944        assert_validator_registration_not_granted(
3945            &state,
3946            treasury,
3947            &before_treasury,
3948            validator,
3949            fingerprint,
3950        );
3951    }
3952
3953    #[test]
3954    fn test_deregister_validator_protocol_pause_rejects_without_deactivation() {
3955        let (processor, state, _alice_kp, _alice, _treasury, genesis_hash) = setup();
3956        let block_producer = Pubkey([42u8; 32]);
3957        let validator_kp = Keypair::generate();
3958        let validator = validator_kp.pubkey();
3959        setup_active_validator(&state, &validator, MIN_VALIDATOR_STAKE);
3960        assert!(
3961            state
3962                .get_stake_pool()
3963                .unwrap()
3964                .get_stake(&validator)
3965                .unwrap()
3966                .is_active
3967        );
3968
3969        put_active_processor_test_restriction(
3970            &state,
3971            RestrictionTarget::ProtocolModule(ProtocolModuleId::Staking),
3972            RestrictionMode::ProtocolPaused,
3973        );
3974
3975        let tx = make_deregister_validator_tx(&validator_kp, validator, genesis_hash);
3976        let result = processor.process_transaction(&tx, &block_producer);
3977        assert!(!result.success);
3978        assert!(result
3979            .error
3980            .as_deref()
3981            .unwrap_or("")
3982            .contains("DeregisterValidator blocked by active Staking protocol pause"));
3983
3984        let pool = state.get_stake_pool().unwrap();
3985        assert!(pool.get_stake(&validator).unwrap().is_active);
3986        assert!(state.get_pending_validator_changes(1).unwrap().is_empty());
3987    }
3988
3989    // ====================================================================
3990    // EVM ADDRESS REGISTRATION (type 12)
3991    // ====================================================================
3992
3993    #[test]
3994    fn test_register_evm_address_success() {
3995        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
3996
3997        let evm_addr: [u8; 20] = [
3998            0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
3999            0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
4000        ];
4001
4002        let mut data = vec![12u8];
4003        data.extend_from_slice(&evm_addr);
4004
4005        let ix = Instruction {
4006            program_id: SYSTEM_PROGRAM_ID,
4007            accounts: vec![alice],
4008            data,
4009        };
4010        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4011        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4012        assert!(
4013            result.success,
4014            "EVM registration should succeed: {:?}",
4015            result.error
4016        );
4017
4018        // Verify mapping exists
4019        let mapped = state.lookup_evm_address(&evm_addr).unwrap();
4020        assert_eq!(mapped, Some(alice));
4021    }
4022
4023    #[test]
4024    fn test_register_evm_address_duplicate_rejected() {
4025        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4026        let bob_kp = Keypair::generate();
4027        let bob = bob_kp.pubkey();
4028        state.put_account(&bob, &Account::new(100, bob)).unwrap();
4029
4030        let evm_addr: [u8; 20] = [0x11; 20];
4031
4032        // Alice registers
4033        let mut data = vec![12u8];
4034        data.extend_from_slice(&evm_addr);
4035        let ix = Instruction {
4036            program_id: SYSTEM_PROGRAM_ID,
4037            accounts: vec![alice],
4038            data: data.clone(),
4039        };
4040        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4041        let r1 = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4042        assert!(r1.success);
4043
4044        // Bob tries to register same EVM address
4045        let ix2 = Instruction {
4046            program_id: SYSTEM_PROGRAM_ID,
4047            accounts: vec![bob],
4048            data,
4049        };
4050        let tx2 = make_signed_tx(&bob_kp, ix2, genesis_hash);
4051        let r2 = processor.process_transaction(&tx2, &Pubkey([42u8; 32]));
4052        assert!(!r2.success, "Duplicate EVM mapping should fail");
4053        assert!(r2.error.unwrap().contains("already mapped"));
4054    }
4055
4056    #[test]
4057    fn test_register_evm_address_invalid_data_rejected() {
4058        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
4059
4060        // Only 10 bytes instead of required 21 (type + 20 addr bytes)
4061        let mut data = vec![12u8];
4062        data.extend_from_slice(&[0xAA; 10]);
4063
4064        let ix = Instruction {
4065            program_id: SYSTEM_PROGRAM_ID,
4066            accounts: vec![alice],
4067            data,
4068        };
4069        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4070        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4071        assert!(!result.success, "Invalid EVM data should fail");
4072        assert!(
4073            result
4074                .error
4075                .as_ref()
4076                .unwrap()
4077                .contains("Invalid EVM address data"),
4078            "Expected 'Invalid EVM address data', got: {:?}",
4079            result.error
4080        );
4081    }
4082
4083    // ====================================================================
4084    // MOSSSTAKE TRANSFER (type 16)
4085    // ====================================================================
4086
4087    #[test]
4088    fn test_mossstake_transfer_success() {
4089        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4090        let bob = Pubkey([80u8; 32]);
4091
4092        // Deposit first: alice deposits 100 LICN into MossStake
4093        let deposit_amount = Account::licn_to_spores(100);
4094        let ix_deposit = Instruction {
4095            program_id: SYSTEM_PROGRAM_ID,
4096            accounts: vec![alice],
4097            data: {
4098                let mut d = vec![13u8]; // MossStake deposit
4099                d.extend_from_slice(&deposit_amount.to_le_bytes());
4100                d
4101            },
4102        };
4103        let tx_dep = make_signed_tx(&alice_kp, ix_deposit, genesis_hash);
4104        let r = processor.process_transaction(&tx_dep, &Pubkey([42u8; 32]));
4105        assert!(r.success, "Deposit should succeed: {:?}", r.error);
4106
4107        // Get alice's stLICN balance
4108        let pool = state.get_mossstake_pool().unwrap();
4109        let (alice_pos, _) = pool
4110            .get_position(&alice)
4111            .expect("Alice should have a position after deposit");
4112        let alice_st_licn = alice_pos.st_licn_amount;
4113        assert!(alice_st_licn > 0, "Alice should have stLICN after deposit");
4114
4115        // Transfer half the stLICN to bob
4116        let transfer_amount = alice_st_licn / 2;
4117        let ix_transfer = Instruction {
4118            program_id: SYSTEM_PROGRAM_ID,
4119            accounts: vec![alice, bob],
4120            data: {
4121                let mut d = vec![16u8]; // MossStake transfer
4122                d.extend_from_slice(&transfer_amount.to_le_bytes());
4123                d
4124            },
4125        };
4126        let tx_xfer = make_signed_tx(&alice_kp, ix_transfer, genesis_hash);
4127        let result = processor.process_transaction(&tx_xfer, &Pubkey([42u8; 32]));
4128        assert!(
4129            result.success,
4130            "MossStake transfer should succeed: {:?}",
4131            result.error
4132        );
4133
4134        // Verify balances
4135        let pool2 = state.get_mossstake_pool().unwrap();
4136        let (bob_pos, _) = pool2
4137            .get_position(&bob)
4138            .expect("Bob should have a position after transfer");
4139        let bob_st_licn = bob_pos.st_licn_amount;
4140        assert_eq!(
4141            bob_st_licn, transfer_amount,
4142            "Bob should have received stLICN"
4143        );
4144    }
4145
4146    // ====================================================================
4147    // REGISTER SYMBOL (type 20)
4148    // ====================================================================
4149
4150    /// Helper: create a fake deployed contract account for symbol registration
4151    fn deploy_fake_contract(state: &StateStore, owner: Pubkey, contract_id: Pubkey) {
4152        let contract = crate::ContractAccount {
4153            code: vec![0x00, 0x61, 0x73, 0x6d], // Minimal WASM header
4154            storage: std::collections::HashMap::new(),
4155            owner,
4156            code_hash: Hash::hash(b"test_code"),
4157            abi: None,
4158            version: 1,
4159            previous_code_hash: None,
4160            upgrade_timelock_epochs: None,
4161            pending_upgrade: None,
4162            lifecycle_status: crate::ContractLifecycleStatus::Active,
4163            lifecycle_updated_slot: 0,
4164            lifecycle_restriction_id: None,
4165        };
4166        let mut acct = Account::new(0, contract_id);
4167        acct.executable = true;
4168        acct.data = serde_json::to_vec(&contract).unwrap();
4169        state.put_account(&contract_id, &acct).unwrap();
4170    }
4171
4172    fn register_contract_symbol_for_test(
4173        state: &StateStore,
4174        owner: Pubkey,
4175        contract_id: Pubkey,
4176        symbol: &str,
4177    ) {
4178        state
4179            .register_symbol(
4180                symbol,
4181                SymbolRegistryEntry {
4182                    symbol: symbol.to_string(),
4183                    program: contract_id,
4184                    owner,
4185                    name: Some(symbol.to_string()),
4186                    template: Some("contract".to_string()),
4187                    metadata: None,
4188                    decimals: None,
4189                },
4190            )
4191            .unwrap();
4192    }
4193
4194    fn configure_incident_guardian_for_test(
4195        state: &StateStore,
4196        governance_authority: Pubkey,
4197        threshold: u8,
4198        signers: Vec<Pubkey>,
4199    ) -> Pubkey {
4200        let guardian_authority =
4201            crate::multisig::derive_incident_guardian_authority(&governance_authority);
4202        state
4203            .set_incident_guardian_authority(&guardian_authority)
4204            .unwrap();
4205        state
4206            .set_governed_wallet_config(
4207                &guardian_authority,
4208                &crate::multisig::GovernedWalletConfig::new(
4209                    threshold,
4210                    signers,
4211                    crate::multisig::INCIDENT_GUARDIAN_LABEL,
4212                ),
4213            )
4214            .unwrap();
4215        guardian_authority
4216    }
4217
4218    fn configure_treasury_executor_for_test(
4219        state: &StateStore,
4220        governance_authority: Pubkey,
4221        threshold: u8,
4222        signers: Vec<Pubkey>,
4223    ) -> Pubkey {
4224        let authority = crate::multisig::derive_treasury_executor_authority(&governance_authority);
4225        state.set_treasury_executor_authority(&authority).unwrap();
4226        state
4227            .set_governed_wallet_config(
4228                &authority,
4229                &crate::multisig::GovernedWalletConfig::new(
4230                    threshold,
4231                    signers,
4232                    crate::multisig::TREASURY_EXECUTOR_LABEL,
4233                )
4234                .with_timelock(1),
4235            )
4236            .unwrap();
4237        authority
4238    }
4239
4240    fn configure_bridge_committee_admin_for_test(
4241        state: &StateStore,
4242        governance_authority: Pubkey,
4243        threshold: u8,
4244        signers: Vec<Pubkey>,
4245    ) -> Pubkey {
4246        let authority =
4247            crate::multisig::derive_bridge_committee_admin_authority(&governance_authority);
4248        state
4249            .set_bridge_committee_admin_authority(&authority)
4250            .unwrap();
4251        state
4252            .set_governed_wallet_config(
4253                &authority,
4254                &crate::multisig::GovernedWalletConfig::new(
4255                    threshold,
4256                    signers,
4257                    crate::multisig::BRIDGE_COMMITTEE_ADMIN_LABEL,
4258                )
4259                .with_timelock(1),
4260            )
4261            .unwrap();
4262        authority
4263    }
4264
4265    fn configure_oracle_committee_admin_for_test(
4266        state: &StateStore,
4267        governance_authority: Pubkey,
4268        threshold: u8,
4269        signers: Vec<Pubkey>,
4270    ) -> Pubkey {
4271        let authority =
4272            crate::multisig::derive_oracle_committee_admin_authority(&governance_authority);
4273        state
4274            .set_oracle_committee_admin_authority(&authority)
4275            .unwrap();
4276        state
4277            .set_governed_wallet_config(
4278                &authority,
4279                &crate::multisig::GovernedWalletConfig::new(
4280                    threshold,
4281                    signers,
4282                    crate::multisig::ORACLE_COMMITTEE_ADMIN_LABEL,
4283                )
4284                .with_timelock(1),
4285            )
4286            .unwrap();
4287        authority
4288    }
4289
4290    fn configure_upgrade_proposer_for_test(
4291        state: &StateStore,
4292        governance_authority: Pubkey,
4293        threshold: u8,
4294        signers: Vec<Pubkey>,
4295    ) -> Pubkey {
4296        let authority = crate::multisig::derive_upgrade_proposer_authority(&governance_authority);
4297        state.set_upgrade_proposer_authority(&authority).unwrap();
4298        state
4299            .set_governed_wallet_config(
4300                &authority,
4301                &crate::multisig::GovernedWalletConfig::new(
4302                    threshold,
4303                    signers,
4304                    crate::multisig::UPGRADE_PROPOSER_LABEL,
4305                )
4306                .with_timelock(1),
4307            )
4308            .unwrap();
4309        authority
4310    }
4311
4312    fn configure_upgrade_veto_guardian_for_test(
4313        state: &StateStore,
4314        governance_authority: Pubkey,
4315        threshold: u8,
4316        signers: Vec<Pubkey>,
4317    ) -> Pubkey {
4318        let authority =
4319            crate::multisig::derive_upgrade_veto_guardian_authority(&governance_authority);
4320        state
4321            .set_upgrade_veto_guardian_authority(&authority)
4322            .unwrap();
4323        state
4324            .set_governed_wallet_config(
4325                &authority,
4326                &crate::multisig::GovernedWalletConfig::new(
4327                    threshold,
4328                    signers,
4329                    crate::multisig::UPGRADE_VETO_GUARDIAN_LABEL,
4330                ),
4331            )
4332            .unwrap();
4333        authority
4334    }
4335
4336    #[test]
4337    fn test_register_symbol_success() {
4338        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4339        let contract_id = Pubkey([90u8; 32]);
4340
4341        deploy_fake_contract(&state, alice, contract_id);
4342
4343        let json_payload = r#"{"symbol":"TLICN","name":"TestLicn","template":"token"}"#;
4344        let mut data = vec![20u8];
4345        data.extend_from_slice(json_payload.as_bytes());
4346
4347        let ix = Instruction {
4348            program_id: SYSTEM_PROGRAM_ID,
4349            accounts: vec![alice, contract_id],
4350            data,
4351        };
4352        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4353        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4354        assert!(
4355            result.success,
4356            "Symbol registration should succeed: {:?}",
4357            result.error
4358        );
4359
4360        // Verify symbol is registered
4361        let entry = state.get_symbol_registry("TLICN").unwrap();
4362        assert!(entry.is_some(), "Symbol TLICN should be in registry");
4363        let e = entry.unwrap();
4364        assert_eq!(e.program, contract_id);
4365        assert_eq!(e.owner, alice);
4366    }
4367
4368    #[test]
4369    fn test_register_symbol_wrong_owner_rejected() {
4370        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4371        let eve_kp = Keypair::generate();
4372        let eve = eve_kp.pubkey();
4373        state.put_account(&eve, &Account::new(100, eve)).unwrap();
4374
4375        let contract_id = Pubkey([91u8; 32]);
4376        // Eve owns the contract, but alice tries to register
4377        deploy_fake_contract(&state, eve, contract_id);
4378
4379        let json_payload = r#"{"symbol":"EVIL","name":"Evil Token"}"#;
4380        let mut data = vec![20u8];
4381        data.extend_from_slice(json_payload.as_bytes());
4382
4383        let ix = Instruction {
4384            program_id: SYSTEM_PROGRAM_ID,
4385            accounts: vec![alice, contract_id],
4386            data,
4387        };
4388        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4389        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4390        assert!(!result.success, "Wrong owner should fail");
4391        assert!(result.error.unwrap().contains("Only the contract owner"));
4392    }
4393
4394    #[test]
4395    fn test_register_symbol_duplicate_different_program_rejected() {
4396        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4397        let contract1 = Pubkey([92u8; 32]);
4398        let contract2 = Pubkey([93u8; 32]);
4399
4400        deploy_fake_contract(&state, alice, contract1);
4401        deploy_fake_contract(&state, alice, contract2);
4402
4403        // Register symbol for contract1
4404        let json = r#"{"symbol":"DUP","name":"Dup Token"}"#;
4405        let mut data = vec![20u8];
4406        data.extend_from_slice(json.as_bytes());
4407
4408        let ix1 = Instruction {
4409            program_id: SYSTEM_PROGRAM_ID,
4410            accounts: vec![alice, contract1],
4411            data: data.clone(),
4412        };
4413        let tx1 = make_signed_tx(&alice_kp, ix1, genesis_hash);
4414        let r1 = processor.process_transaction(&tx1, &Pubkey([42u8; 32]));
4415        assert!(
4416            r1.success,
4417            "First registration should succeed: {:?}",
4418            r1.error
4419        );
4420
4421        // Try to register same symbol for contract2
4422        let ix2 = Instruction {
4423            program_id: SYSTEM_PROGRAM_ID,
4424            accounts: vec![alice, contract2],
4425            data,
4426        };
4427        let tx2 = make_signed_tx(&alice_kp, ix2, genesis_hash);
4428        let r2 = processor.process_transaction(&tx2, &Pubkey([42u8; 32]));
4429        assert!(
4430            !r2.success,
4431            "Duplicate symbol on different contract should fail"
4432        );
4433        assert!(r2.error.unwrap().contains("already registered"));
4434    }
4435
4436    #[test]
4437    fn test_register_symbol_rejects_overlong_fields() {
4438        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4439        let contract_id = Pubkey([94u8; 32]);
4440
4441        deploy_fake_contract(&state, alice, contract_id);
4442
4443        let payload = serde_json::json!({
4444            "symbol": "S".repeat(MAX_SYMBOL_REGISTRY_SYMBOL_LEN + 1),
4445            "name": "N".repeat(MAX_SYMBOL_REGISTRY_NAME_LEN + 1),
4446            "template": "T".repeat(MAX_SYMBOL_REGISTRY_TEMPLATE_LEN + 1),
4447            "metadata": {
4448                "k".repeat(MAX_SYMBOL_REGISTRY_METADATA_KEY_LEN + 1): "value"
4449            }
4450        });
4451        let mut data = vec![20u8];
4452        data.extend_from_slice(payload.to_string().as_bytes());
4453
4454        let ix = Instruction {
4455            program_id: SYSTEM_PROGRAM_ID,
4456            accounts: vec![alice, contract_id],
4457            data,
4458        };
4459        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4460        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4461        assert!(!result.success, "overlong symbol registration must fail");
4462        assert!(
4463            result
4464                .error
4465                .as_deref()
4466                .unwrap_or_default()
4467                .contains("exceeds"),
4468            "unexpected: {:?}",
4469            result.error
4470        );
4471    }
4472
4473    // ====================================================================
4474    // UTILITY FUNCTIONS
4475    // ====================================================================
4476
4477    // AUDIT-FIX INFO-01: test_reputation_fee_discount_removed removed along with the function.
4478
4479    #[test]
4480    fn test_get_trust_tier() {
4481        assert_eq!(get_trust_tier(0), 0);
4482        assert_eq!(get_trust_tier(99), 0);
4483        assert_eq!(get_trust_tier(100), 1);
4484        assert_eq!(get_trust_tier(499), 1);
4485        assert_eq!(get_trust_tier(500), 2);
4486        assert_eq!(get_trust_tier(999), 2);
4487        assert_eq!(get_trust_tier(1000), 3);
4488        assert_eq!(get_trust_tier(4999), 3);
4489        assert_eq!(get_trust_tier(5000), 4);
4490        assert_eq!(get_trust_tier(9999), 4);
4491        assert_eq!(get_trust_tier(10000), 5);
4492        assert_eq!(get_trust_tier(99999), 5);
4493    }
4494
4495    // ====================================================================
4496    // SIMULATE TRANSACTION
4497    // ====================================================================
4498
4499    #[test]
4500    fn test_simulate_valid_transfer() {
4501        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
4502        let bob = Pubkey([2u8; 32]);
4503
4504        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
4505        let sim = processor.simulate_transaction(&tx);
4506
4507        assert!(
4508            sim.success,
4509            "Simulation should succeed for valid tx: {:?}",
4510            sim.error
4511        );
4512        assert!(sim.fee > 0, "Fee should be non-zero");
4513        assert!(!sim.logs.is_empty(), "Logs should be populated");
4514    }
4515
4516    #[test]
4517    fn test_simulate_zero_blockhash_rejected() {
4518        let (processor, _state, alice_kp, alice, _treasury, _genesis_hash) = setup();
4519        let bob = Pubkey([2u8; 32]);
4520
4521        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, Hash::default());
4522        let sim = processor.simulate_transaction(&tx);
4523
4524        assert!(
4525            !sim.success,
4526            "Zero blockhash should be rejected in simulation"
4527        );
4528        assert!(sim.error.unwrap().contains("Zero blockhash"));
4529    }
4530
4531    #[test]
4532    fn test_simulate_bad_blockhash_rejected() {
4533        let (processor, _state, alice_kp, alice, _treasury, _genesis_hash) = setup();
4534        let bob = Pubkey([2u8; 32]);
4535
4536        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, Hash::hash(b"not_a_real_block"));
4537        let sim = processor.simulate_transaction(&tx);
4538
4539        assert!(
4540            !sim.success,
4541            "Invalid blockhash should be rejected in simulation"
4542        );
4543        assert!(sim.error.unwrap().contains("Blockhash not found"));
4544    }
4545
4546    #[test]
4547    fn test_simulate_unsigned_rejected() {
4548        let (processor, _state, _alice_kp, alice, _treasury, genesis_hash) = setup();
4549        let bob = Pubkey([2u8; 32]);
4550
4551        let mut data = vec![0u8];
4552        data.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
4553        let ix = Instruction {
4554            program_id: SYSTEM_PROGRAM_ID,
4555            accounts: vec![alice, bob],
4556            data,
4557        };
4558        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
4559        let tx = Transaction::new(message); // No signatures
4560
4561        let sim = processor.simulate_transaction(&tx);
4562        assert!(!sim.success, "Unsigned tx should fail simulation");
4563        assert!(sim.error.unwrap().contains("Missing"));
4564    }
4565
4566    #[test]
4567    fn test_simulate_insufficient_balance() {
4568        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4569        let bob = Pubkey([2u8; 32]);
4570
4571        // Drain alice's balance
4572        let mut acct = state.get_account(&alice).unwrap().unwrap();
4573        acct.spores = 0;
4574        acct.spendable = 0;
4575        state.put_account(&alice, &acct).unwrap();
4576
4577        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
4578        let sim = processor.simulate_transaction(&tx);
4579
4580        assert!(!sim.success, "Should fail with insufficient balance");
4581        assert!(sim.error.unwrap().contains("Insufficient balance"));
4582    }
4583
4584    #[test]
4585    fn test_simulate_contract_call_uses_top_level_runtime_context() {
4586        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4587        let lichenid_program = Pubkey([44u8; 32]);
4588        let rep_key = crate::contract::lichenid_reputation_storage_key(&alice);
4589        let rep_data = 42u64.to_le_bytes().to_vec();
4590        let contract_addr =
4591            install_test_contract_account(&state, alice, reputation_reader_contract_code(&rep_key));
4592
4593        state
4594            .put_account(&alice, &Account::new(Account::licn_to_spores(10), alice))
4595            .unwrap();
4596        state
4597            .put_contract_storage(&contract_addr, b"pm_lichenid_addr", &lichenid_program.0)
4598            .unwrap();
4599        state
4600            .put_contract_storage(&lichenid_program, &rep_key, &rep_data)
4601            .unwrap();
4602
4603        let ix = Instruction {
4604            program_id: CONTRACT_PROGRAM_ID,
4605            accounts: vec![alice, contract_addr],
4606            data: crate::ContractInstruction::Call {
4607                function: "read_reputation".to_string(),
4608                args: Vec::new(),
4609                value: 0,
4610            }
4611            .serialize()
4612            .unwrap(),
4613        };
4614        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4615        let sim = processor.simulate_transaction(&tx);
4616
4617        assert!(sim.success, "simulation should succeed: {:?}", sim.error);
4618        assert_eq!(sim.return_data, Some(rep_data));
4619    }
4620
4621    // ====================================================================
4622    // UNKNOWN INSTRUCTION TYPE
4623    // ====================================================================
4624
4625    #[test]
4626    fn test_unknown_system_instruction_rejected() {
4627        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
4628
4629        let ix = Instruction {
4630            program_id: SYSTEM_PROGRAM_ID,
4631            accounts: vec![alice],
4632            data: vec![255u8], // Unknown type
4633        };
4634        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4635        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4636        assert!(!result.success, "Unknown instruction type should fail");
4637        assert!(result.error.unwrap().contains("Unknown system instruction"));
4638    }
4639
4640    #[test]
4641    fn test_empty_instruction_data_rejected() {
4642        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
4643
4644        let ix = Instruction {
4645            program_id: SYSTEM_PROGRAM_ID,
4646            accounts: vec![alice],
4647            data: vec![],
4648        };
4649        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
4650        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4651        assert!(!result.success, "Empty instruction data should fail");
4652        assert!(result.error.unwrap().contains("Empty instruction data"));
4653    }
4654
4655    #[test]
4656    fn test_fee_split_sums_to_100() {
4657        let cfg = FeeConfig::default_from_constants();
4658        let total = cfg.fee_burn_percent
4659            + cfg.fee_producer_percent
4660            + cfg.fee_voters_percent
4661            + cfg.fee_treasury_percent
4662            + cfg.fee_community_percent;
4663        assert_eq!(
4664            total, 100,
4665            "fee split percentages must sum to 100, got {total}"
4666        );
4667        // Verify individual values match design spec (40/30/10/10/10)
4668        assert_eq!(cfg.fee_burn_percent, 40);
4669        assert_eq!(cfg.fee_producer_percent, 30);
4670        assert_eq!(cfg.fee_voters_percent, 10);
4671        assert_eq!(cfg.fee_treasury_percent, 10);
4672        assert_eq!(cfg.fee_community_percent, 10);
4673    }
4674
4675    // ====================================================================
4676    // GOVERNED WALLET MULTI-SIG TESTS
4677    // ====================================================================
4678
4679    #[test]
4680    fn test_ecosystem_grant_requires_multisig() {
4681        // Standard transfer from a governed wallet must be rejected.
4682        let (processor, state, _alice_kp, alice, _treasury, genesis_hash) = setup();
4683        let eco_kp = Keypair::generate();
4684        let eco = eco_kp.pubkey();
4685        let recipient = Pubkey([99u8; 32]);
4686
4687        // Fund the ecosystem wallet
4688        let eco_acct = Account::new(Account::licn_to_spores(1000), Pubkey([0u8; 32]));
4689        state.put_account(&eco, &eco_acct).unwrap();
4690
4691        // Configure as governed wallet (threshold=2, signers=[alice, eco])
4692        let config = crate::multisig::GovernedWalletConfig::new(
4693            2,
4694            vec![alice, eco],
4695            "ecosystem_partnerships",
4696        );
4697        state.set_governed_wallet_config(&eco, &config).unwrap();
4698
4699        // Standard transfer (type 0) from governed wallet → REJECTED
4700        let tx = make_transfer_tx(&eco_kp, eco, recipient, 100, genesis_hash);
4701        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
4702        assert!(
4703            !result.success,
4704            "Standard transfer from governed wallet should be rejected"
4705        );
4706        assert!(
4707            result
4708                .error
4709                .as_ref()
4710                .unwrap()
4711                .contains("multi-sig proposal"),
4712            "Error should mention multi-sig requirement, got: {}",
4713            result.error.unwrap()
4714        );
4715
4716        // Recipient should NOT have received anything
4717        assert_eq!(state.get_balance(&recipient).unwrap(), 0);
4718    }
4719
4720    fn put_active_processor_test_restriction(
4721        state: &StateStore,
4722        target: RestrictionTarget,
4723        mode: RestrictionMode,
4724    ) -> u64 {
4725        let id = state.next_restriction_id().unwrap();
4726        let record = RestrictionRecord {
4727            id,
4728            target,
4729            mode,
4730            status: RestrictionStatus::Active,
4731            reason: RestrictionReason::TestnetDrill,
4732            evidence_hash: None,
4733            evidence_uri_hash: None,
4734            proposer: Pubkey([0xA1; 32]),
4735            authority: Pubkey([0xA2; 32]),
4736            approval_authority: None,
4737            created_slot: 0,
4738            created_epoch: 0,
4739            expires_at_slot: None,
4740            supersedes: None,
4741            lifted_by: None,
4742            lifted_slot: None,
4743            lift_reason: None,
4744        };
4745        state.put_restriction(&record).unwrap();
4746        id
4747    }
4748
4749    fn lift_processor_test_restriction(state: &StateStore, restriction_id: u64, lifted_by: Pubkey) {
4750        let mut record = state
4751            .get_restriction(restriction_id)
4752            .unwrap()
4753            .expect("restriction should exist");
4754        record.status = RestrictionStatus::Lifted;
4755        record.lifted_by = Some(lifted_by);
4756        record.lifted_slot = Some(state.get_last_slot().unwrap());
4757        record.lift_reason = Some(RestrictionLiftReason::TestnetDrillComplete);
4758        state.put_restriction(&record).unwrap();
4759    }
4760
4761    fn governed_transfer_propose_tx(
4762        proposer_kp: &Keypair,
4763        proposer: Pubkey,
4764        source: Pubkey,
4765        recipient: Pubkey,
4766        amount: u64,
4767        recent_blockhash: Hash,
4768    ) -> Transaction {
4769        let mut data = vec![21u8];
4770        data.extend_from_slice(&amount.to_le_bytes());
4771        let ix = Instruction {
4772            program_id: SYSTEM_PROGRAM_ID,
4773            accounts: vec![proposer, source, recipient],
4774            data,
4775        };
4776        make_signed_tx(proposer_kp, ix, recent_blockhash)
4777    }
4778
4779    fn governed_transfer_control_tx(
4780        signer_kp: &Keypair,
4781        signer: Pubkey,
4782        opcode: u8,
4783        proposal_id: u64,
4784        recent_blockhash: Hash,
4785    ) -> Transaction {
4786        let mut data = vec![opcode];
4787        data.extend_from_slice(&proposal_id.to_le_bytes());
4788        let ix = Instruction {
4789            program_id: SYSTEM_PROGRAM_ID,
4790            accounts: vec![signer],
4791            data,
4792        };
4793        make_signed_tx(signer_kp, ix, recent_blockhash)
4794    }
4795
4796    #[test]
4797    fn test_governed_wallet_direct_transfer_still_requires_proposal_when_restricted() {
4798        let (processor, state, _alice_kp, alice, _treasury, genesis_hash) = setup();
4799        let validator = Pubkey([42u8; 32]);
4800        let gov_kp = Keypair::generate();
4801        let gov = gov_kp.pubkey();
4802        let recipient = Pubkey([0x91; 32]);
4803
4804        state.put_account(&gov, &Account::new(1_000, gov)).unwrap();
4805        state
4806            .set_governed_wallet_config(
4807                &gov,
4808                &crate::multisig::GovernedWalletConfig::new(
4809                    2,
4810                    vec![alice, gov],
4811                    "ecosystem_partnerships",
4812                ),
4813            )
4814            .unwrap();
4815        put_active_processor_test_restriction(
4816            &state,
4817            RestrictionTarget::Account(gov),
4818            RestrictionMode::OutgoingOnly,
4819        );
4820
4821        let tx = make_transfer_tx(&gov_kp, gov, recipient, 10, genesis_hash);
4822        let result = processor.process_transaction(&tx, &validator);
4823        assert!(!result.success);
4824        assert!(result
4825            .error
4826            .as_deref()
4827            .unwrap_or("")
4828            .contains("multi-sig proposal"));
4829        assert!(!result
4830            .error
4831            .as_deref()
4832            .unwrap_or("")
4833            .contains("restriction"));
4834        assert_eq!(state.get_balance(&recipient).unwrap(), 0);
4835    }
4836
4837    #[test]
4838    fn test_governed_transfer_source_restriction_blocks_execution_without_losing_proposal() {
4839        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4840        let validator = Pubkey([42u8; 32]);
4841        let bob_kp = Keypair::generate();
4842        let bob = bob_kp.pubkey();
4843        let gov = Pubkey([0x92; 32]);
4844        let recipient = Pubkey([0x93; 32]);
4845        let amount = Account::licn_to_spores(50);
4846
4847        state.put_account(&bob, &Account::new(1_000, bob)).unwrap();
4848        state.put_account(&gov, &Account::new(1_000, gov)).unwrap();
4849        state
4850            .set_governed_wallet_config(
4851                &gov,
4852                &crate::multisig::GovernedWalletConfig::new(
4853                    2,
4854                    vec![alice, bob, gov],
4855                    "ecosystem_partnerships",
4856                )
4857                .with_timelock(1)
4858                .with_transfer_velocity_policy(
4859                    crate::multisig::GovernedTransferVelocityPolicy::new(
4860                        amount * 10,
4861                        amount * 2,
4862                        0,
4863                        0,
4864                        0,
4865                        0,
4866                    ),
4867                ),
4868            )
4869            .unwrap();
4870        let restriction_id = put_active_processor_test_restriction(
4871            &state,
4872            RestrictionTarget::Account(gov),
4873            RestrictionMode::OutgoingOnly,
4874        );
4875
4876        let propose_tx =
4877            governed_transfer_propose_tx(&alice_kp, alice, gov, recipient, amount, genesis_hash);
4878        let result = processor.process_transaction(&propose_tx, &validator);
4879        assert!(result.success, "proposal failed: {:?}", result.error);
4880
4881        let approve_tx = governed_transfer_control_tx(&bob_kp, bob, 22, 1, genesis_hash);
4882        let result = processor.process_transaction(&approve_tx, &validator);
4883        assert!(result.success, "approval failed: {:?}", result.error);
4884        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
4885        assert_eq!(proposal.approvals.len(), 2);
4886        assert!(!proposal.executed);
4887
4888        let execute_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
4889        let execute_tx = governed_transfer_control_tx(&alice_kp, alice, 32, 1, execute_blockhash);
4890        let result = processor.process_transaction(&execute_tx, &validator);
4891        assert!(!result.success);
4892        assert!(result
4893            .error
4894            .as_deref()
4895            .unwrap_or("")
4896            .contains("sender account restriction"));
4897
4898        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
4899        assert_eq!(proposal.approvals.len(), 2);
4900        assert!(!proposal.executed);
4901        assert_eq!(state.get_balance(&recipient).unwrap(), 0);
4902        let day_bucket = SLOTS_PER_EPOCH / SECONDS_PER_DAY;
4903        assert_eq!(
4904            state
4905                .get_governed_transfer_day_volume(&gov, day_bucket)
4906                .unwrap(),
4907            0
4908        );
4909
4910        lift_processor_test_restriction(&state, restriction_id, alice);
4911        let retry_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH + 1);
4912        let retry_tx = governed_transfer_control_tx(&bob_kp, bob, 32, 1, retry_blockhash);
4913        let result = processor.process_transaction(&retry_tx, &validator);
4914        assert!(result.success, "retry failed: {:?}", result.error);
4915        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
4916        assert!(proposal.executed);
4917        assert_eq!(state.get_balance(&recipient).unwrap(), amount);
4918        assert_eq!(
4919            state
4920                .get_governed_transfer_day_volume(&gov, day_bucket)
4921                .unwrap(),
4922            amount
4923        );
4924    }
4925
4926    #[test]
4927    fn test_governed_transfer_recipient_restriction_blocks_execution_without_losing_proposal() {
4928        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
4929        let validator = Pubkey([42u8; 32]);
4930        let bob_kp = Keypair::generate();
4931        let bob = bob_kp.pubkey();
4932        let gov = Pubkey([0x94; 32]);
4933        let recipient = Pubkey([0x95; 32]);
4934        let amount = Account::licn_to_spores(50);
4935
4936        state.put_account(&bob, &Account::new(1_000, bob)).unwrap();
4937        state.put_account(&gov, &Account::new(1_000, gov)).unwrap();
4938        state
4939            .set_governed_wallet_config(
4940                &gov,
4941                &crate::multisig::GovernedWalletConfig::new(
4942                    2,
4943                    vec![alice, bob, gov],
4944                    "ecosystem_partnerships",
4945                )
4946                .with_timelock(1)
4947                .with_transfer_velocity_policy(
4948                    crate::multisig::GovernedTransferVelocityPolicy::new(
4949                        amount * 10,
4950                        amount * 2,
4951                        0,
4952                        0,
4953                        0,
4954                        0,
4955                    ),
4956                ),
4957            )
4958            .unwrap();
4959        let restriction_id = put_active_processor_test_restriction(
4960            &state,
4961            RestrictionTarget::Account(recipient),
4962            RestrictionMode::IncomingOnly,
4963        );
4964
4965        let propose_tx =
4966            governed_transfer_propose_tx(&alice_kp, alice, gov, recipient, amount, genesis_hash);
4967        let result = processor.process_transaction(&propose_tx, &validator);
4968        assert!(result.success, "proposal failed: {:?}", result.error);
4969
4970        let approve_tx = governed_transfer_control_tx(&bob_kp, bob, 22, 1, genesis_hash);
4971        let result = processor.process_transaction(&approve_tx, &validator);
4972        assert!(result.success, "approval failed: {:?}", result.error);
4973
4974        let execute_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
4975        let execute_tx = governed_transfer_control_tx(&alice_kp, alice, 32, 1, execute_blockhash);
4976        let result = processor.process_transaction(&execute_tx, &validator);
4977        assert!(!result.success);
4978        assert!(result
4979            .error
4980            .as_deref()
4981            .unwrap_or("")
4982            .contains("recipient account restriction"));
4983
4984        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
4985        assert_eq!(proposal.approvals.len(), 2);
4986        assert!(!proposal.executed);
4987        assert_eq!(state.get_balance(&recipient).unwrap(), 0);
4988        let day_bucket = SLOTS_PER_EPOCH / SECONDS_PER_DAY;
4989        assert_eq!(
4990            state
4991                .get_governed_transfer_day_volume(&gov, day_bucket)
4992                .unwrap(),
4993            0
4994        );
4995
4996        lift_processor_test_restriction(&state, restriction_id, alice);
4997        let retry_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH + 1);
4998        let retry_tx = governed_transfer_control_tx(&bob_kp, bob, 32, 1, retry_blockhash);
4999        let result = processor.process_transaction(&retry_tx, &validator);
5000        assert!(result.success, "retry failed: {:?}", result.error);
5001        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5002        assert!(proposal.executed);
5003        assert_eq!(state.get_balance(&recipient).unwrap(), amount);
5004        assert_eq!(
5005            state
5006                .get_governed_transfer_day_volume(&gov, day_bucket)
5007                .unwrap(),
5008            amount
5009        );
5010    }
5011
5012    #[test]
5013    fn test_governed_proposal_lifecycle() {
5014        // Propose → approve → auto-execute lifecycle for governed wallet.
5015        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
5016        let bob_kp = Keypair::generate();
5017        let bob = bob_kp.pubkey();
5018        let eco_kp = Keypair::generate();
5019        let eco = eco_kp.pubkey();
5020        let recipient = Pubkey([99u8; 32]);
5021
5022        // Fund participants
5023        let fund = Account::licn_to_spores(1000);
5024        state
5025            .put_account(&eco, &Account::new(fund, Pubkey([0u8; 32])))
5026            .unwrap();
5027        state
5028            .put_account(&alice, &Account::new(fund, alice))
5029            .unwrap();
5030        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
5031
5032        // Configure governed wallet (threshold=2, signers=[alice, bob, eco])
5033        let config = crate::multisig::GovernedWalletConfig::new(
5034            2,
5035            vec![alice, bob, eco],
5036            "ecosystem_partnerships",
5037        );
5038        state.set_governed_wallet_config(&eco, &config).unwrap();
5039
5040        let transfer_amount = Account::licn_to_spores(50);
5041
5042        // Step 1: Alice proposes a governed transfer (type 21)
5043        let mut propose_data = vec![21u8];
5044        propose_data.extend_from_slice(&transfer_amount.to_le_bytes());
5045        let propose_ix = Instruction {
5046            program_id: SYSTEM_PROGRAM_ID,
5047            accounts: vec![alice, eco, recipient],
5048            data: propose_data,
5049        };
5050        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
5051        let result = processor.process_transaction(&propose_tx, &Pubkey([42u8; 32]));
5052        assert!(
5053            result.success,
5054            "Proposal should succeed: {:?}",
5055            result.error
5056        );
5057
5058        // Verify proposal exists but is NOT executed yet
5059        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5060        assert_eq!(proposal.approvals.len(), 1);
5061        assert_eq!(proposal.approvals[0], alice);
5062        assert!(
5063            !proposal.executed,
5064            "Proposal should not be executed with only 1 approval"
5065        );
5066        assert_eq!(
5067            state.get_balance(&recipient).unwrap(),
5068            0,
5069            "Recipient should not have funds yet"
5070        );
5071
5072        // Step 2: Bob approves (type 22) → reaches threshold → auto-executes
5073        let mut approve_data = vec![22u8];
5074        approve_data.extend_from_slice(&1u64.to_le_bytes()); // proposal_id = 1
5075        let approve_ix = Instruction {
5076            program_id: SYSTEM_PROGRAM_ID,
5077            accounts: vec![bob],
5078            data: approve_data,
5079        };
5080        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
5081        let result = processor.process_transaction(&approve_tx, &Pubkey([42u8; 32]));
5082        assert!(
5083            result.success,
5084            "Approval should succeed: {:?}",
5085            result.error
5086        );
5087
5088        // Verify proposal is now executed
5089        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5090        assert!(
5091            proposal.executed,
5092            "Proposal should be executed after meeting threshold"
5093        );
5094        assert_eq!(proposal.approvals.len(), 2);
5095
5096        // Verify transfer happened
5097        assert_eq!(
5098            state.get_balance(&recipient).unwrap(),
5099            transfer_amount,
5100            "Recipient should have received the transfer"
5101        );
5102    }
5103
5104    #[test]
5105    fn test_governed_proposal_timelock_requires_execute() {
5106        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
5107        let bob_kp = Keypair::generate();
5108        let bob = bob_kp.pubkey();
5109        let eco_kp = Keypair::generate();
5110        let eco = eco_kp.pubkey();
5111        let recipient = Pubkey([98u8; 32]);
5112
5113        let fund = Account::licn_to_spores(1_000);
5114        state
5115            .put_account(&eco, &Account::new(fund, Pubkey([0u8; 32])))
5116            .unwrap();
5117        state
5118            .put_account(&alice, &Account::new(fund, alice))
5119            .unwrap();
5120        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
5121        state.set_last_slot(0).unwrap();
5122
5123        let config = crate::multisig::GovernedWalletConfig::new(
5124            2,
5125            vec![alice, bob, eco],
5126            "community_treasury",
5127        )
5128        .with_timelock(1);
5129        state.set_governed_wallet_config(&eco, &config).unwrap();
5130
5131        let transfer_amount = Account::licn_to_spores(25);
5132
5133        let mut propose_data = vec![21u8];
5134        propose_data.extend_from_slice(&transfer_amount.to_le_bytes());
5135        let propose_ix = Instruction {
5136            program_id: SYSTEM_PROGRAM_ID,
5137            accounts: vec![alice, eco, recipient],
5138            data: propose_data,
5139        };
5140        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
5141        let result = processor.process_transaction(&propose_tx, &Pubkey([42u8; 32]));
5142        assert!(
5143            result.success,
5144            "Proposal should succeed: {:?}",
5145            result.error
5146        );
5147
5148        let mut approve_data = vec![22u8];
5149        approve_data.extend_from_slice(&1u64.to_le_bytes());
5150        let approve_ix = Instruction {
5151            program_id: SYSTEM_PROGRAM_ID,
5152            accounts: vec![bob],
5153            data: approve_data,
5154        };
5155        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
5156        let result = processor.process_transaction(&approve_tx, &Pubkey([42u8; 32]));
5157        assert!(
5158            result.success,
5159            "Approval should succeed: {:?}",
5160            result.error
5161        );
5162
5163        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5164        assert_eq!(proposal.execute_after_epoch, 1);
5165        assert!(!proposal.executed, "Proposal should remain timelocked");
5166        assert_eq!(state.get_balance(&recipient).unwrap(), 0);
5167
5168        let mut execute_data = vec![32u8];
5169        execute_data.extend_from_slice(&1u64.to_le_bytes());
5170        let execute_ix = Instruction {
5171            program_id: SYSTEM_PROGRAM_ID,
5172            accounts: vec![bob],
5173            data: execute_data.clone(),
5174        };
5175        let execute_tx = make_signed_tx(&bob_kp, execute_ix, genesis_hash);
5176        let result = processor.process_transaction(&execute_tx, &Pubkey([42u8; 32]));
5177        assert!(
5178            !result.success,
5179            "Execution should fail before timelock expires"
5180        );
5181        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
5182
5183        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
5184
5185        let execute_ix = Instruction {
5186            program_id: SYSTEM_PROGRAM_ID,
5187            accounts: vec![alice],
5188            data: execute_data,
5189        };
5190        let execute_tx = make_signed_tx(&alice_kp, execute_ix, fresh_blockhash);
5191        let result = processor.process_transaction(&execute_tx, &Pubkey([42u8; 32]));
5192        assert!(
5193            result.success,
5194            "Execution should succeed: {:?}",
5195            result.error
5196        );
5197
5198        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5199        assert!(
5200            proposal.executed,
5201            "Proposal should be executed after timelock"
5202        );
5203        assert_eq!(state.get_balance(&recipient).unwrap(), transfer_amount);
5204    }
5205
5206    #[test]
5207    fn test_governed_transfer_velocity_policy_rejects_amount_over_cap() {
5208        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
5209        let validator = Pubkey([42u8; 32]);
5210        let governed_wallet = Pubkey([0x71; 32]);
5211        let recipient = Pubkey([0x72; 32]);
5212
5213        state
5214            .put_account(&governed_wallet, &Account::new(1_000, governed_wallet))
5215            .unwrap();
5216        state
5217            .set_governed_wallet_config(
5218                &governed_wallet,
5219                &crate::multisig::GovernedWalletConfig::new(
5220                    1,
5221                    vec![alice],
5222                    "ecosystem_partnerships",
5223                )
5224                .with_transfer_velocity_policy(
5225                    crate::multisig::GovernedTransferVelocityPolicy::new(50, 100, 0, 0, 0, 0),
5226                ),
5227            )
5228            .unwrap();
5229
5230        let mut propose_data = vec![21u8];
5231        propose_data.extend_from_slice(&60u64.to_le_bytes());
5232        let propose_ix = Instruction {
5233            program_id: SYSTEM_PROGRAM_ID,
5234            accounts: vec![alice, governed_wallet, recipient],
5235            data: propose_data,
5236        };
5237        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
5238        let result = processor.process_transaction(&propose_tx, &validator);
5239        assert!(!result.success);
5240        assert!(result
5241            .error
5242            .as_deref()
5243            .unwrap_or("")
5244            .contains("per-transfer cap"));
5245    }
5246
5247    #[test]
5248    fn test_governed_transfer_velocity_policy_snapshots_escalation() {
5249        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
5250        let validator = Pubkey([42u8; 32]);
5251        let bob_kp = Keypair::generate();
5252        let bob = bob_kp.pubkey();
5253        let governed_wallet = Pubkey([0x73; 32]);
5254        let recipient = Pubkey([0x74; 32]);
5255
5256        state.put_account(&bob, &Account::new(1_000, bob)).unwrap();
5257        state
5258            .put_account(&governed_wallet, &Account::new(1_000, governed_wallet))
5259            .unwrap();
5260        state
5261            .set_governed_wallet_config(
5262                &governed_wallet,
5263                &crate::multisig::GovernedWalletConfig::new(
5264                    1,
5265                    vec![alice, bob],
5266                    "community_treasury",
5267                )
5268                .with_transfer_velocity_policy(
5269                    crate::multisig::GovernedTransferVelocityPolicy::new(200, 200, 50, 90, 1, 3),
5270                ),
5271            )
5272            .unwrap();
5273
5274        let mut propose_data = vec![21u8];
5275        propose_data.extend_from_slice(&60u64.to_le_bytes());
5276        let propose_ix = Instruction {
5277            program_id: SYSTEM_PROGRAM_ID,
5278            accounts: vec![alice, governed_wallet, recipient],
5279            data: propose_data,
5280        };
5281        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
5282        let result = processor.process_transaction(&propose_tx, &validator);
5283        assert!(result.success, "proposal failed: {:?}", result.error);
5284
5285        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5286        assert_eq!(proposal.threshold, 2);
5287        assert_eq!(proposal.execute_after_epoch, 1);
5288        assert_eq!(
5289            proposal.velocity_tier,
5290            crate::multisig::GovernedTransferVelocityTier::Elevated
5291        );
5292        assert_eq!(proposal.daily_cap_spores, 200);
5293        assert!(!proposal.executed);
5294
5295        let mut approve_data = vec![22u8];
5296        approve_data.extend_from_slice(&1u64.to_le_bytes());
5297        let approve_ix = Instruction {
5298            program_id: SYSTEM_PROGRAM_ID,
5299            accounts: vec![bob],
5300            data: approve_data,
5301        };
5302        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
5303        let result = processor.process_transaction(&approve_tx, &validator);
5304        assert!(result.success, "approval failed: {:?}", result.error);
5305        assert!(!state.get_governed_proposal(1).unwrap().unwrap().executed);
5306
5307        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
5308        let mut execute_data = vec![32u8];
5309        execute_data.extend_from_slice(&1u64.to_le_bytes());
5310        let execute_ix = Instruction {
5311            program_id: SYSTEM_PROGRAM_ID,
5312            accounts: vec![alice],
5313            data: execute_data,
5314        };
5315        let execute_tx = make_signed_tx(&alice_kp, execute_ix, fresh_blockhash);
5316        let result = processor.process_transaction(&execute_tx, &validator);
5317        assert!(result.success, "execution failed: {:?}", result.error);
5318        assert_eq!(state.get_balance(&recipient).unwrap(), 60);
5319    }
5320
5321    #[test]
5322    fn test_governed_transfer_daily_cap_defers_until_next_day() {
5323        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
5324        let validator = Pubkey([42u8; 32]);
5325        let governed_wallet = Pubkey([0x75; 32]);
5326        let first_recipient = Pubkey([0x76; 32]);
5327        let second_recipient = Pubkey([0x77; 32]);
5328
5329        state
5330            .put_account(&governed_wallet, &Account::new(1_000, governed_wallet))
5331            .unwrap();
5332        state
5333            .set_governed_wallet_config(
5334                &governed_wallet,
5335                &crate::multisig::GovernedWalletConfig::new(1, vec![alice], "community_treasury")
5336                    .with_transfer_velocity_policy(
5337                        crate::multisig::GovernedTransferVelocityPolicy::new(200, 100, 0, 0, 0, 0),
5338                    ),
5339            )
5340            .unwrap();
5341
5342        let mut first_propose_data = vec![21u8];
5343        first_propose_data.extend_from_slice(&60u64.to_le_bytes());
5344        let first_propose_ix = Instruction {
5345            program_id: SYSTEM_PROGRAM_ID,
5346            accounts: vec![alice, governed_wallet, first_recipient],
5347            data: first_propose_data,
5348        };
5349        let first_propose_tx = make_signed_tx(&alice_kp, first_propose_ix, genesis_hash);
5350        let result = processor.process_transaction(&first_propose_tx, &validator);
5351        assert!(result.success, "first transfer failed: {:?}", result.error);
5352        assert!(state.get_governed_proposal(1).unwrap().unwrap().executed);
5353
5354        let mut second_propose_data = vec![21u8];
5355        second_propose_data.extend_from_slice(&50u64.to_le_bytes());
5356        let second_propose_ix = Instruction {
5357            program_id: SYSTEM_PROGRAM_ID,
5358            accounts: vec![alice, governed_wallet, second_recipient],
5359            data: second_propose_data,
5360        };
5361        let second_propose_tx = make_signed_tx(&alice_kp, second_propose_ix, genesis_hash);
5362        let result = processor.process_transaction(&second_propose_tx, &validator);
5363        assert!(result.success, "second proposal failed: {:?}", result.error);
5364
5365        let second_proposal = state.get_governed_proposal(2).unwrap().unwrap();
5366        assert!(!second_proposal.executed);
5367        assert_eq!(state.get_balance(&second_recipient).unwrap(), 0);
5368        assert_eq!(
5369            state
5370                .get_governed_transfer_day_volume(&governed_wallet, 0)
5371                .unwrap(),
5372            60
5373        );
5374
5375        let mut execute_data = vec![32u8];
5376        execute_data.extend_from_slice(&2u64.to_le_bytes());
5377        let execute_ix = Instruction {
5378            program_id: SYSTEM_PROGRAM_ID,
5379            accounts: vec![alice],
5380            data: execute_data.clone(),
5381        };
5382        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
5383        let result = processor.process_transaction(&execute_tx, &validator);
5384        assert!(!result.success);
5385        assert!(result.error.as_deref().unwrap_or("").contains("daily cap"));
5386
5387        let fresh_blockhash = advance_test_slot(&state, SECONDS_PER_DAY);
5388        let execute_ix = Instruction {
5389            program_id: SYSTEM_PROGRAM_ID,
5390            accounts: vec![alice],
5391            data: execute_data,
5392        };
5393        let execute_tx = make_signed_tx(&alice_kp, execute_ix, fresh_blockhash);
5394        let result = processor.process_transaction(&execute_tx, &validator);
5395        assert!(
5396            result.success,
5397            "deferred execute failed: {:?}",
5398            result.error
5399        );
5400        assert!(state.get_governed_proposal(2).unwrap().unwrap().executed);
5401        assert_eq!(state.get_balance(&second_recipient).unwrap(), 50);
5402        assert_eq!(
5403            state
5404                .get_governed_transfer_day_volume(&governed_wallet, 1)
5405                .unwrap(),
5406            50
5407        );
5408    }
5409
5410    #[test]
5411    fn test_reserve_pool_requires_supermajority() {
5412        // Reserve pool with threshold=3 requires more approvals than ecosystem (threshold=2).
5413        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
5414        let bob_kp = Keypair::generate();
5415        let bob = bob_kp.pubkey();
5416        let reserve_kp = Keypair::generate();
5417        let reserve = reserve_kp.pubkey();
5418        let recipient = Pubkey([88u8; 32]);
5419
5420        // Fund participants
5421        let fund = Account::licn_to_spores(1000);
5422        state
5423            .put_account(&reserve, &Account::new(fund, Pubkey([0u8; 32])))
5424            .unwrap();
5425        state
5426            .put_account(&alice, &Account::new(fund, alice))
5427            .unwrap();
5428        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
5429
5430        // Configure reserve_pool as governed wallet (threshold=3 — supermajority)
5431        let config = crate::multisig::GovernedWalletConfig::new(
5432            3,
5433            vec![alice, bob, reserve],
5434            "reserve_pool",
5435        );
5436        state.set_governed_wallet_config(&reserve, &config).unwrap();
5437
5438        let transfer_amount = Account::licn_to_spores(10);
5439
5440        // Propose
5441        let mut data = vec![21u8];
5442        data.extend_from_slice(&transfer_amount.to_le_bytes());
5443        let ix = Instruction {
5444            program_id: SYSTEM_PROGRAM_ID,
5445            accounts: vec![alice, reserve, recipient],
5446            data,
5447        };
5448        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
5449        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5450        assert!(result.success);
5451
5452        // First approval (Bob) — still not enough (2 of 3)
5453        let mut data = vec![22u8];
5454        data.extend_from_slice(&1u64.to_le_bytes());
5455        let ix = Instruction {
5456            program_id: SYSTEM_PROGRAM_ID,
5457            accounts: vec![bob],
5458            data,
5459        };
5460        let tx = make_signed_tx(&bob_kp, ix, genesis_hash);
5461        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5462        assert!(result.success);
5463
5464        // Verify NOT executed yet (2 approvals, need 3)
5465        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5466        assert!(
5467            !proposal.executed,
5468            "Should NOT be executed with only 2/3 approvals"
5469        );
5470        assert_eq!(state.get_balance(&recipient).unwrap(), 0);
5471
5472        // Third approval (reserve keypair) → threshold met → auto-execute
5473        let mut data = vec![22u8];
5474        data.extend_from_slice(&1u64.to_le_bytes());
5475        let ix = Instruction {
5476            program_id: SYSTEM_PROGRAM_ID,
5477            accounts: vec![reserve],
5478            data,
5479        };
5480        let tx = make_signed_tx(&reserve_kp, ix, genesis_hash);
5481        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5482        assert!(
5483            result.success,
5484            "Third approval should succeed: {:?}",
5485            result.error
5486        );
5487
5488        // Verify executed
5489        let proposal = state.get_governed_proposal(1).unwrap().unwrap();
5490        assert!(proposal.executed, "Should be executed with 3/3 approvals");
5491        assert_eq!(state.get_balance(&recipient).unwrap(), transfer_amount);
5492    }
5493
5494    // ─── Shielded pool processor tests ──────────────────────────────
5495
5496    #[cfg(feature = "zk")]
5497    #[test]
5498    fn test_shield_rejects_short_data() {
5499        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
5500
5501        // Only 21 bytes provided (need at least 42)
5502        let mut data = vec![23u8];
5503        data.extend_from_slice(&[0u8; 20]);
5504
5505        let ix = Instruction {
5506            program_id: SYSTEM_PROGRAM_ID,
5507            accounts: vec![alice],
5508            data,
5509        };
5510        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
5511        let mut tx = Transaction::new(msg);
5512        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
5513
5514        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5515        assert!(!result.success);
5516        assert!(
5517            result.error.as_ref().unwrap().contains("insufficient data"),
5518            "Expected insufficient data error, got: {:?}",
5519            result.error
5520        );
5521    }
5522
5523    #[cfg(feature = "zk")]
5524    #[test]
5525    fn test_shield_rejects_zero_amount() {
5526        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
5527
5528        let mut data = vec![23u8];
5529        data.extend_from_slice(&0u64.to_le_bytes()); // zero amount
5530        data.extend_from_slice(&[0xAA; 32]); // commitment
5531        data.extend_from_slice(&[0xBB; 128]); // fake proof
5532
5533        let ix = Instruction {
5534            program_id: SYSTEM_PROGRAM_ID,
5535            accounts: vec![alice],
5536            data,
5537        };
5538        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
5539        let mut tx = Transaction::new(msg);
5540        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
5541
5542        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5543        assert!(!result.success);
5544        assert!(
5545            result.error.as_ref().unwrap().contains("non-zero"),
5546            "Expected non-zero error, got: {:?}",
5547            result.error
5548        );
5549    }
5550
5551    #[cfg(feature = "zk")]
5552    #[test]
5553    fn test_shield_rejects_no_accounts() {
5554        let (processor, _state, alice_kp, _alice, _treasury, genesis_hash) = setup();
5555
5556        let mut data = vec![23u8];
5557        data.extend_from_slice(&100u64.to_le_bytes());
5558        data.extend_from_slice(&[0xAA; 32]);
5559        data.extend_from_slice(&[0xBB; 128]);
5560
5561        let ix = Instruction {
5562            program_id: SYSTEM_PROGRAM_ID,
5563            accounts: vec![], // no accounts!
5564            data,
5565        };
5566        // We still need at least one account for fee payer, so we put alice in a second ix
5567        // Actually the processor checks accounts on the instruction level — let's just test
5568        // that the error message is correct
5569        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
5570        let mut tx = Transaction::new(msg);
5571        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
5572
5573        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5574        assert!(!result.success);
5575        // It might fail at fee payer extraction or at the shield handler
5576        assert!(result.error.is_some());
5577    }
5578
5579    #[cfg(feature = "zk")]
5580    #[test]
5581    fn test_shield_rejects_invalid_proof_bytes() {
5582        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
5583
5584        let mut data = vec![23u8];
5585        data.extend_from_slice(&100u64.to_le_bytes());
5586        data.extend_from_slice(&[0xAA; 32]); // bogus commitment
5587        data.extend_from_slice(&[0xFF; 7]); // invalid proof bytes
5588
5589        let ix = Instruction {
5590            program_id: SYSTEM_PROGRAM_ID,
5591            accounts: vec![alice],
5592            data,
5593        };
5594        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
5595        let mut tx = Transaction::new(msg);
5596        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
5597
5598        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5599        assert!(!result.success, "Invalid proof bytes should fail");
5600        assert!(
5601            result.error.as_ref().unwrap().contains("proof"),
5602            "Expected proof-related error, got: {:?}",
5603            result.error
5604        );
5605    }
5606
5607    #[cfg(feature = "zk")]
5608    #[test]
5609    fn test_shield_accepts_native_proof_without_verifier_keys() {
5610        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
5611        use crate::zk::{
5612            circuits::shield::ShieldCircuit, commitment_hash, random_scalar_bytes, Prover,
5613        };
5614
5615        let amount = 100u64;
5616        let blinding = random_scalar_bytes();
5617        let commitment = commitment_hash(amount, &blinding);
5618        let circuit = ShieldCircuit::new_bytes(amount, amount, blinding, commitment);
5619        let proof = Prover::new().prove_shield(circuit).expect("prove shield");
5620
5621        let mut data = vec![23u8];
5622        data.extend_from_slice(&amount.to_le_bytes());
5623        data.extend_from_slice(&commitment);
5624        data.extend_from_slice(&proof.proof_bytes);
5625
5626        let ix = Instruction {
5627            program_id: SYSTEM_PROGRAM_ID,
5628            accounts: vec![alice],
5629            data,
5630        };
5631        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
5632        let mut tx = Transaction::new(msg);
5633        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
5634
5635        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5636        assert!(result.success, "native STARK verifier should not need VKs");
5637    }
5638
5639    #[cfg(feature = "zk")]
5640    #[test]
5641    fn test_shield_full_e2e_with_processor() {
5642        use crate::zk::{
5643            circuits::shield::ShieldCircuit, commitment_hash, random_scalar_bytes, Prover,
5644        };
5645
5646        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5647
5648        // 1. Build shield witness
5649        let amount = 500_000_000u64; // 0.5 LICN in spores
5650        let blinding = random_scalar_bytes();
5651        let commitment = commitment_hash(amount, &blinding);
5652
5653        let circuit = ShieldCircuit::new_bytes(amount, amount, blinding, commitment);
5654
5655        // 2. Generate proof
5656        let zk_proof = Prover::new().prove_shield(circuit).unwrap();
5657
5658        // 3. Build instruction data with the encrypted-note recovery payload
5659        let encrypted_note = format!("a1:{}:{}", "00".repeat(12), "11".repeat(16));
5660        let ephemeral_pk = hex::encode([0x22u8; 32]);
5661        let note_payload = serde_json::json!({
5662            "commitment": hex::encode(commitment),
5663            "encrypted_note": encrypted_note,
5664            "ephemeral_pk": ephemeral_pk,
5665        })
5666        .to_string()
5667        .into_bytes();
5668        let mut data = vec![23u8];
5669        data.extend_from_slice(&amount.to_le_bytes());
5670        data.extend_from_slice(&commitment);
5671        data.extend_from_slice(b"LNP1");
5672        data.extend_from_slice(&(zk_proof.proof_bytes.len() as u32).to_le_bytes());
5673        data.extend_from_slice(&zk_proof.proof_bytes);
5674        data.extend_from_slice(&(note_payload.len() as u32).to_le_bytes());
5675        data.extend_from_slice(&note_payload);
5676
5677        let ix = Instruction {
5678            program_id: SYSTEM_PROGRAM_ID,
5679            accounts: vec![alice],
5680            data,
5681        };
5682        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
5683        let mut tx = Transaction::new(msg);
5684        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
5685
5686        // 4. Process transaction
5687        let alice_balance_before = state.get_balance(&alice).unwrap();
5688        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
5689        assert!(result.success, "Shield should succeed: {:?}", result.error);
5690
5691        // 5. Verify state changes
5692        let alice_balance_after = state.get_balance(&alice).unwrap();
5693        // Alice should have less balance (amount + fee deducted)
5694        assert!(
5695            alice_balance_after < alice_balance_before,
5696            "Alice balance should decrease after shield"
5697        );
5698        assert_eq!(
5699            alice_balance_before - alice_balance_after - result.fee_paid,
5700            amount,
5701            "Balance decrease minus fee should equal shielded amount"
5702        );
5703
5704        // Pool state should be updated
5705        let pool = state.get_shielded_pool_state().unwrap();
5706        assert_eq!(pool.commitment_count, 1);
5707        assert_eq!(pool.total_shielded, amount);
5708
5709        // Commitment should be stored
5710        let stored_commitment = state.get_shielded_commitment(0).unwrap();
5711        assert_eq!(stored_commitment, Some(commitment));
5712        let stored_note_payload = state.get_shielded_note_payload(0).unwrap();
5713        assert_eq!(
5714            stored_note_payload.as_deref(),
5715            Some(note_payload.as_slice())
5716        );
5717
5718        // Merkle root should be updated to reflect the single leaf
5719        let mut expected_tree = crate::zk::MerkleTree::new();
5720        expected_tree.insert(commitment);
5721        assert_eq!(pool.merkle_root, expected_tree.root());
5722    }
5723
5724    /// Renamed setup helper for shielded tests to avoid name collision
5725    #[cfg(feature = "zk")]
5726    fn setup_() -> (TxProcessor, StateStore, Keypair, Pubkey, Pubkey, Hash) {
5727        setup()
5728    }
5729
5730    #[cfg(feature = "zk")]
5731    fn make_invalid_shield_tx(
5732        kp: &Keypair,
5733        sender: Pubkey,
5734        amount: u64,
5735        commitment: [u8; 32],
5736        recent_blockhash: Hash,
5737    ) -> Transaction {
5738        let mut data = vec![23u8];
5739        data.extend_from_slice(&amount.to_le_bytes());
5740        data.extend_from_slice(&commitment);
5741        data.extend_from_slice(&[0xFF; 7]);
5742
5743        make_signed_tx(
5744            kp,
5745            Instruction {
5746                program_id: SYSTEM_PROGRAM_ID,
5747                accounts: vec![sender],
5748                data,
5749            },
5750            recent_blockhash,
5751        )
5752    }
5753
5754    #[cfg(feature = "zk")]
5755    fn make_invalid_unshield_tx(
5756        kp: &Keypair,
5757        recipient: Pubkey,
5758        amount: u64,
5759        nullifier: [u8; 32],
5760        merkle_root: [u8; 32],
5761        recent_blockhash: Hash,
5762    ) -> Transaction {
5763        use crate::zk::{recipient_hash, recipient_preimage_from_bytes};
5764
5765        let recipient_bytes = recipient_hash(&recipient_preimage_from_bytes(recipient.0));
5766        let mut data = vec![24u8];
5767        data.extend_from_slice(&amount.to_le_bytes());
5768        data.extend_from_slice(&nullifier);
5769        data.extend_from_slice(&merkle_root);
5770        data.extend_from_slice(&recipient_bytes);
5771        data.extend_from_slice(&[0xFF; 7]);
5772
5773        make_signed_tx(
5774            kp,
5775            Instruction {
5776                program_id: SYSTEM_PROGRAM_ID,
5777                accounts: vec![recipient],
5778                data,
5779            },
5780            recent_blockhash,
5781        )
5782    }
5783
5784    #[cfg(feature = "zk")]
5785    fn make_invalid_shielded_transfer_tx(
5786        kp: &Keypair,
5787        fee_payer: Pubkey,
5788        nullifier_a: [u8; 32],
5789        nullifier_b: [u8; 32],
5790        recent_blockhash: Hash,
5791    ) -> Transaction {
5792        let mut data = vec![25u8];
5793        data.extend_from_slice(&nullifier_a);
5794        data.extend_from_slice(&nullifier_b);
5795        data.extend_from_slice(&[0xC1; 32]);
5796        data.extend_from_slice(&[0xC2; 32]);
5797        data.extend_from_slice(&[0u8; 32]);
5798        data.extend_from_slice(&[0xFF; 7]);
5799
5800        make_signed_tx(
5801            kp,
5802            Instruction {
5803                program_id: SYSTEM_PROGRAM_ID,
5804                accounts: vec![fee_payer],
5805                data,
5806            },
5807            recent_blockhash,
5808        )
5809    }
5810
5811    #[cfg(feature = "zk")]
5812    fn assert_shielded_pool_unchanged(state: &StateStore, before: &crate::zk::ShieldedPoolState) {
5813        let after = state.get_shielded_pool_state().unwrap();
5814        assert_eq!(after.merkle_root, before.merkle_root);
5815        assert_eq!(after.commitment_count, before.commitment_count);
5816        assert_eq!(after.total_shielded, before.total_shielded);
5817        assert_eq!(after.nullifier_count, before.nullifier_count);
5818        assert_eq!(after.shield_count, before.shield_count);
5819        assert_eq!(after.unshield_count, before.unshield_count);
5820        assert_eq!(after.transfer_count, before.transfer_count);
5821    }
5822
5823    #[cfg(feature = "zk")]
5824    #[test]
5825    fn test_shield_rejects_outgoing_restricted_sender_without_pool_mutation() {
5826        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5827        let validator = Pubkey([42u8; 32]);
5828        let before_pool = state.get_shielded_pool_state().unwrap();
5829        let before_balance = state.get_balance(&alice).unwrap();
5830
5831        put_active_processor_test_restriction(
5832            &state,
5833            RestrictionTarget::Account(alice),
5834            RestrictionMode::OutgoingOnly,
5835        );
5836
5837        let commitment = [0xA7; 32];
5838        let tx = make_invalid_shield_tx(&alice_kp, alice, 100, commitment, genesis_hash);
5839        let result = processor.process_transaction(&tx, &validator);
5840        assert!(!result.success);
5841        assert!(result
5842            .error
5843            .as_deref()
5844            .unwrap_or("")
5845            .contains("Shield blocked by active sender account restriction"));
5846
5847        assert_shielded_pool_unchanged(&state, &before_pool);
5848        assert_eq!(state.get_shielded_commitment(0).unwrap(), None);
5849        let after_balance = state.get_balance(&alice).unwrap();
5850        assert_eq!(before_balance - after_balance, result.fee_paid);
5851    }
5852
5853    #[cfg(feature = "zk")]
5854    #[test]
5855    fn test_shield_rejects_native_frozen_amount_without_pool_mutation() {
5856        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5857        let validator = Pubkey([42u8; 32]);
5858        let before_pool = state.get_shielded_pool_state().unwrap();
5859        let before_balance = state.get_balance(&alice).unwrap();
5860        let spendable = state.get_account(&alice).unwrap().unwrap().spendable;
5861
5862        put_active_processor_test_restriction(
5863            &state,
5864            RestrictionTarget::AccountAsset {
5865                account: alice,
5866                asset: NATIVE_LICN_ASSET_ID,
5867            },
5868            RestrictionMode::FrozenAmount { amount: spendable },
5869        );
5870
5871        let commitment = [0xA8; 32];
5872        let tx = make_invalid_shield_tx(&alice_kp, alice, 100, commitment, genesis_hash);
5873        let result = processor.process_transaction(&tx, &validator);
5874        assert!(!result.success);
5875        assert!(result
5876            .error
5877            .as_deref()
5878            .unwrap_or("")
5879            .contains("Shield blocked by active sender native account-asset restriction"));
5880
5881        assert_shielded_pool_unchanged(&state, &before_pool);
5882        assert_eq!(state.get_shielded_commitment(0).unwrap(), None);
5883        let after_balance = state.get_balance(&alice).unwrap();
5884        assert_eq!(before_balance - after_balance, result.fee_paid);
5885    }
5886
5887    #[cfg(feature = "zk")]
5888    #[test]
5889    fn test_shield_protocol_pause_rejects_deposit_without_pool_mutation() {
5890        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5891        let validator = Pubkey([42u8; 32]);
5892        let before_pool = state.get_shielded_pool_state().unwrap();
5893        let before_balance = state.get_balance(&alice).unwrap();
5894
5895        put_active_processor_test_restriction(
5896            &state,
5897            RestrictionTarget::ProtocolModule(ProtocolModuleId::Shielded),
5898            RestrictionMode::ProtocolPaused,
5899        );
5900
5901        let commitment = [0xA9; 32];
5902        let tx = make_invalid_shield_tx(&alice_kp, alice, 100, commitment, genesis_hash);
5903        let result = processor.process_transaction(&tx, &validator);
5904        assert!(!result.success);
5905        assert!(result
5906            .error
5907            .as_deref()
5908            .unwrap_or("")
5909            .contains("Shield blocked by active Shielded protocol pause"));
5910
5911        assert_shielded_pool_unchanged(&state, &before_pool);
5912        assert_eq!(state.get_shielded_commitment(0).unwrap(), None);
5913        let after_balance = state.get_balance(&alice).unwrap();
5914        assert_eq!(before_balance - after_balance, result.fee_paid);
5915    }
5916
5917    #[cfg(feature = "zk")]
5918    #[test]
5919    fn test_unshield_rejects_incoming_restricted_recipient_without_spending_nullifier() {
5920        use crate::zk::random_scalar_bytes;
5921
5922        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5923        let validator = Pubkey([42u8; 32]);
5924        let before_pool = state.get_shielded_pool_state().unwrap();
5925        let before_balance = state.get_balance(&alice).unwrap();
5926        let nullifier = random_scalar_bytes();
5927
5928        put_active_processor_test_restriction(
5929            &state,
5930            RestrictionTarget::Account(alice),
5931            RestrictionMode::IncomingOnly,
5932        );
5933
5934        let tx =
5935            make_invalid_unshield_tx(&alice_kp, alice, 100, nullifier, [0xEE; 32], genesis_hash);
5936        let result = processor.process_transaction(&tx, &validator);
5937        assert!(!result.success);
5938        assert!(result
5939            .error
5940            .as_deref()
5941            .unwrap_or("")
5942            .contains("Unshield blocked by active recipient account restriction"));
5943
5944        assert!(!state.is_nullifier_spent(&nullifier).unwrap());
5945        assert_shielded_pool_unchanged(&state, &before_pool);
5946        let after_balance = state.get_balance(&alice).unwrap();
5947        assert_eq!(before_balance - after_balance, result.fee_paid);
5948    }
5949
5950    #[cfg(feature = "zk")]
5951    #[test]
5952    fn test_unshield_rejects_native_incoming_restricted_recipient_without_spending_nullifier() {
5953        use crate::zk::random_scalar_bytes;
5954
5955        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5956        let validator = Pubkey([42u8; 32]);
5957        let before_pool = state.get_shielded_pool_state().unwrap();
5958        let before_balance = state.get_balance(&alice).unwrap();
5959        let nullifier = random_scalar_bytes();
5960
5961        put_active_processor_test_restriction(
5962            &state,
5963            RestrictionTarget::AccountAsset {
5964                account: alice,
5965                asset: NATIVE_LICN_ASSET_ID,
5966            },
5967            RestrictionMode::IncomingOnly,
5968        );
5969
5970        let tx =
5971            make_invalid_unshield_tx(&alice_kp, alice, 100, nullifier, [0xEF; 32], genesis_hash);
5972        let result = processor.process_transaction(&tx, &validator);
5973        assert!(!result.success);
5974        assert!(result
5975            .error
5976            .as_deref()
5977            .unwrap_or("")
5978            .contains("Unshield blocked by active recipient native account-asset restriction"));
5979
5980        assert!(!state.is_nullifier_spent(&nullifier).unwrap());
5981        assert_shielded_pool_unchanged(&state, &before_pool);
5982        let after_balance = state.get_balance(&alice).unwrap();
5983        assert_eq!(before_balance - after_balance, result.fee_paid);
5984    }
5985
5986    #[cfg(feature = "zk")]
5987    #[test]
5988    fn test_shielded_transfer_protocol_pause_rejects_before_nullifier_mutation() {
5989        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
5990        let validator = Pubkey([42u8; 32]);
5991        let before_pool = state.get_shielded_pool_state().unwrap();
5992        let nullifier_a = [0xF1; 32];
5993        let nullifier_b = [0xF2; 32];
5994
5995        put_active_processor_test_restriction(
5996            &state,
5997            RestrictionTarget::ProtocolModule(ProtocolModuleId::Shielded),
5998            RestrictionMode::ProtocolPaused,
5999        );
6000
6001        let tx = make_invalid_shielded_transfer_tx(
6002            &alice_kp,
6003            alice,
6004            nullifier_a,
6005            nullifier_b,
6006            genesis_hash,
6007        );
6008        let result = processor.process_transaction(&tx, &validator);
6009        assert!(!result.success);
6010        assert!(result
6011            .error
6012            .as_deref()
6013            .unwrap_or("")
6014            .contains("ShieldedTransfer blocked by active Shielded protocol pause"));
6015
6016        assert!(!state.is_nullifier_spent(&nullifier_a).unwrap());
6017        assert!(!state.is_nullifier_spent(&nullifier_b).unwrap());
6018        assert_shielded_pool_unchanged(&state, &before_pool);
6019    }
6020
6021    #[cfg(feature = "zk")]
6022    #[test]
6023    fn test_unshield_rejects_short_data() {
6024        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6025
6026        let mut data = vec![24u8];
6027        data.extend_from_slice(&[0u8; 50]); // too short (need at least 106 bytes total)
6028
6029        let ix = Instruction {
6030            program_id: SYSTEM_PROGRAM_ID,
6031            accounts: vec![alice],
6032            data,
6033        };
6034        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6035        let mut tx = Transaction::new(msg);
6036        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6037
6038        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
6039        assert!(!result.success);
6040        assert!(result.error.as_ref().unwrap().contains("insufficient data"));
6041    }
6042
6043    #[cfg(feature = "zk")]
6044    #[test]
6045    fn test_shield_batch_updates_merkle_root_with_prior_batch_commitments() {
6046        use crate::zk::{
6047            circuits::shield::ShieldCircuit, commitment_hash, random_scalar_bytes, Prover,
6048        };
6049
6050        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup_();
6051        let validator = Pubkey([42u8; 32]);
6052
6053        let amount_a = 100u64;
6054        let blinding_a = random_scalar_bytes();
6055        let commitment_a = commitment_hash(amount_a, &blinding_a);
6056        let proof_a = Prover::new()
6057            .prove_shield(ShieldCircuit::new_bytes(
6058                amount_a,
6059                amount_a,
6060                blinding_a,
6061                commitment_a,
6062            ))
6063            .unwrap();
6064
6065        let amount_b = 200u64;
6066        let blinding_b = random_scalar_bytes();
6067        let commitment_b = commitment_hash(amount_b, &blinding_b);
6068        let proof_b = Prover::new()
6069            .prove_shield(ShieldCircuit::new_bytes(
6070                amount_b,
6071                amount_b,
6072                blinding_b,
6073                commitment_b,
6074            ))
6075            .unwrap();
6076
6077        let mut data_a = vec![23u8];
6078        data_a.extend_from_slice(&amount_a.to_le_bytes());
6079        data_a.extend_from_slice(&commitment_a);
6080        data_a.extend_from_slice(&proof_a.proof_bytes);
6081
6082        let mut data_b = vec![23u8];
6083        data_b.extend_from_slice(&amount_b.to_le_bytes());
6084        data_b.extend_from_slice(&commitment_b);
6085        data_b.extend_from_slice(&proof_b.proof_bytes);
6086
6087        let msg = crate::transaction::Message::new(
6088            vec![
6089                Instruction {
6090                    program_id: SYSTEM_PROGRAM_ID,
6091                    accounts: vec![alice],
6092                    data: data_a,
6093                },
6094                Instruction {
6095                    program_id: SYSTEM_PROGRAM_ID,
6096                    accounts: vec![alice],
6097                    data: data_b,
6098                },
6099            ],
6100            genesis_hash,
6101        );
6102        let mut tx = Transaction::new(msg);
6103        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6104
6105        let result = processor.process_transaction(&tx, &validator);
6106        assert!(
6107            result.success,
6108            "batched shield deposits should succeed: {:?}",
6109            result.error
6110        );
6111
6112        let pool = state.get_shielded_pool_state().unwrap();
6113        assert_eq!(pool.commitment_count, 2);
6114        let mut tree = crate::zk::MerkleTree::new();
6115        tree.insert(commitment_a);
6116        tree.insert(commitment_b);
6117        assert_eq!(pool.merkle_root, tree.root());
6118    }
6119
6120    #[cfg(feature = "zk")]
6121    #[test]
6122    fn test_unshield_rejects_recipient_mismatch() {
6123        use crate::zk::{recipient_hash, recipient_preimage_from_bytes};
6124
6125        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6126
6127        // Build valid-length unshield payload but with recipient input bound to a different account.
6128        let amount = 100u64;
6129        let nullifier = [0x11u8; 32];
6130        let merkle_root = [0u8; 32];
6131
6132        // Deliberately mismatch by hashing a different pubkey than `alice`.
6133        let other_pubkey = Pubkey([0x22u8; 32]);
6134        let other_recipient = recipient_hash(&recipient_preimage_from_bytes(other_pubkey.0));
6135
6136        let mut data = vec![24u8];
6137        data.extend_from_slice(&amount.to_le_bytes());
6138        data.extend_from_slice(&nullifier);
6139        data.extend_from_slice(&merkle_root);
6140        data.extend_from_slice(&other_recipient);
6141        data.extend_from_slice(&[0u8; 128]);
6142
6143        let ix = Instruction {
6144            program_id: SYSTEM_PROGRAM_ID,
6145            accounts: vec![alice],
6146            data,
6147        };
6148        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6149        let mut tx = Transaction::new(msg);
6150        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6151
6152        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
6153        assert!(!result.success);
6154        assert!(
6155            result
6156                .error
6157                .as_ref()
6158                .unwrap()
6159                .contains("recipient public input does not match recipient account"),
6160            "unexpected error: {:?}",
6161            result.error
6162        );
6163    }
6164
6165    #[cfg(feature = "zk")]
6166    #[test]
6167    fn test_transfer_rejects_short_data() {
6168        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6169
6170        let mut data = vec![25u8];
6171        data.extend_from_slice(&[0u8; 100]); // too short (need at least 162 bytes total)
6172
6173        let ix = Instruction {
6174            program_id: SYSTEM_PROGRAM_ID,
6175            accounts: vec![alice],
6176            data,
6177        };
6178        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6179        let mut tx = Transaction::new(msg);
6180        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6181
6182        let result = processor.process_transaction(&tx, &Pubkey([42u8; 32]));
6183        assert!(!result.success);
6184        assert!(result.error.as_ref().unwrap().contains("insufficient data"));
6185    }
6186
6187    // ─── Graduated Rent Tests ────────────────────────────────────────────────
6188
6189    #[test]
6190    fn test_graduated_rent_below_free_tier() {
6191        // Accounts with ≤ 2KB data pay zero rent
6192        assert_eq!(compute_graduated_rent(0, 100), 0);
6193        assert_eq!(compute_graduated_rent(1024, 100), 0);
6194        assert_eq!(compute_graduated_rent(2048, 100), 0);
6195    }
6196
6197    #[test]
6198    fn test_graduated_rent_tier1() {
6199        // 3KB total → 1KB billable → 1KB × 1× rate
6200        assert_eq!(compute_graduated_rent(3 * 1024, 100), 100);
6201        // 10KB total → 8KB billable → 8KB × 1× rate
6202        assert_eq!(compute_graduated_rent(10 * 1024, 100), 800);
6203    }
6204
6205    #[test]
6206    fn test_graduated_rent_tier2() {
6207        // 11KB total → 9KB billable → 8KB @1x + 1KB @2x
6208        assert_eq!(compute_graduated_rent(11 * 1024, 100), 800 + 200);
6209        // 50KB total → 48KB billable → 8KB @1x + 40KB @2x
6210        assert_eq!(compute_graduated_rent(50 * 1024, 100), 800 + 8000);
6211        // 100KB total → 98KB billable → 8KB @1x + 90KB @2x
6212        assert_eq!(compute_graduated_rent(100 * 1024, 100), 800 + 18000);
6213    }
6214
6215    #[test]
6216    fn test_graduated_rent_tier3() {
6217        // 101KB total → 99KB billable → 8KB @1x + 90KB @2x + 1KB @4x
6218        assert_eq!(compute_graduated_rent(101 * 1024, 100), 800 + 18000 + 400);
6219        // 200KB total → 198KB billable → 8KB @1x + 90KB @2x + 100KB @4x
6220        assert_eq!(compute_graduated_rent(200 * 1024, 100), 800 + 18000 + 40000);
6221    }
6222
6223    #[test]
6224    fn test_graduated_rent_partial_kb() {
6225        // 2049 bytes → 1 byte over free tier → rounds up to 1KB
6226        assert_eq!(compute_graduated_rent(2049, 100), 100);
6227        // 2048 + 512 = 2560 → 512 bytes over → rounds up to 1KB
6228        assert_eq!(compute_graduated_rent(2560, 100), 100);
6229    }
6230
6231    #[test]
6232    fn test_graduated_rent_zero_rate() {
6233        assert_eq!(compute_graduated_rent(100 * 1024, 0), 0);
6234    }
6235
6236    // ======== Durable Nonce Tests ========
6237
6238    /// Helper: create a nonce-initialize instruction
6239    fn make_nonce_init_ix(funder: Pubkey, nonce_pk: Pubkey, authority: Pubkey) -> Instruction {
6240        let mut data = vec![28u8, 0u8]; // type=28, sub=0 (Initialize)
6241        data.extend_from_slice(&authority.0);
6242        Instruction {
6243            program_id: SYSTEM_PROGRAM_ID,
6244            accounts: vec![funder, nonce_pk],
6245            data,
6246        }
6247    }
6248
6249    /// Helper: create a nonce-advance instruction
6250    fn make_nonce_advance_ix(authority: Pubkey, nonce_pk: Pubkey) -> Instruction {
6251        let data = vec![28u8, 1u8]; // type=28, sub=1 (Advance)
6252        Instruction {
6253            program_id: SYSTEM_PROGRAM_ID,
6254            accounts: vec![authority, nonce_pk],
6255            data,
6256        }
6257    }
6258
6259    fn make_nonce_withdraw_ix(
6260        authority: Pubkey,
6261        nonce_pk: Pubkey,
6262        recipient: Pubkey,
6263        amount: u64,
6264    ) -> Instruction {
6265        let mut data = vec![28u8, 2u8];
6266        data.extend_from_slice(&amount.to_le_bytes());
6267        Instruction {
6268            program_id: SYSTEM_PROGRAM_ID,
6269            accounts: vec![authority, nonce_pk, recipient],
6270            data,
6271        }
6272    }
6273
6274    fn initialize_test_nonce(
6275        processor: &TxProcessor,
6276        funder_kp: &Keypair,
6277        funder: Pubkey,
6278        nonce_pk: Pubkey,
6279        authority: Pubkey,
6280        recent_blockhash: Hash,
6281        validator: &Pubkey,
6282    ) {
6283        let ix = make_nonce_init_ix(funder, nonce_pk, authority);
6284        let tx = make_signed_tx(funder_kp, ix, recent_blockhash);
6285        let result = processor.process_transaction(&tx, validator);
6286        assert!(
6287            result.success,
6288            "Nonce initialization should succeed: {:?}",
6289            result.error
6290        );
6291    }
6292
6293    fn assert_failed_nonce_withdraw_keeps_nonce_open(
6294        state: &StateStore,
6295        nonce_pk: Pubkey,
6296        recipient: Pubkey,
6297        expected_error: &Option<String>,
6298        expected_error_fragment: &str,
6299        before_nonce_account: &Account,
6300    ) {
6301        assert!(
6302            expected_error
6303                .as_ref()
6304                .unwrap()
6305                .contains(expected_error_fragment),
6306            "Expected error containing '{}', got: {:?}",
6307            expected_error_fragment,
6308            expected_error
6309        );
6310        let after_nonce_account = state.get_account(&nonce_pk).unwrap().unwrap();
6311        assert_eq!(after_nonce_account.spores, before_nonce_account.spores);
6312        assert_eq!(
6313            after_nonce_account.spendable,
6314            before_nonce_account.spendable
6315        );
6316        assert_eq!(after_nonce_account.data, before_nonce_account.data);
6317        assert_eq!(after_nonce_account.data[0], NONCE_ACCOUNT_MARKER);
6318        assert_eq!(state.get_balance(&recipient).unwrap_or(0), 0);
6319    }
6320
6321    #[test]
6322    fn test_nonce_initialize() {
6323        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6324        let validator = Pubkey([42u8; 32]);
6325        let nonce_pk = Pubkey([99u8; 32]);
6326
6327        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6328        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
6329        let mut tx = Transaction::new(message);
6330        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6331
6332        let result = processor.process_transaction(&tx, &validator);
6333        assert!(
6334            result.success,
6335            "NonceInit should succeed: {:?}",
6336            result.error
6337        );
6338
6339        // Verify nonce account exists with expected state
6340        let nonce_acct = state.get_account(&nonce_pk).unwrap().unwrap();
6341        assert_eq!(nonce_acct.spores, NONCE_ACCOUNT_MIN_BALANCE);
6342        assert_eq!(nonce_acct.owner, SYSTEM_PROGRAM_ID);
6343        assert_eq!(nonce_acct.data[0], NONCE_ACCOUNT_MARKER);
6344
6345        let ns = TxProcessor::decode_nonce_state(&nonce_acct.data).unwrap();
6346        assert_eq!(ns.authority, alice);
6347        assert_eq!(ns.blockhash, genesis_hash);
6348        assert_eq!(ns.fee_per_signature, BASE_FEE);
6349    }
6350
6351    #[test]
6352    fn test_nonce_initialize_rejects_existing_account() {
6353        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6354        let validator = Pubkey([42u8; 32]);
6355        let nonce_pk = Pubkey([99u8; 32]);
6356
6357        // Pre-create the nonce account
6358        state
6359            .put_account(&nonce_pk, &Account::new(0, nonce_pk))
6360            .unwrap();
6361
6362        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6363        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
6364        let mut tx = Transaction::new(message);
6365        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6366
6367        let result = processor.process_transaction(&tx, &validator);
6368        assert!(!result.success);
6369        assert!(
6370            result.error.as_ref().unwrap().contains("already exists"),
6371            "Expected 'already exists' error, got: {:?}",
6372            result.error
6373        );
6374    }
6375
6376    #[test]
6377    fn test_nonce_initialize_rejects_insufficient_funds() {
6378        let temp_dir = tempdir().unwrap();
6379        let state = StateStore::open(temp_dir.path()).unwrap();
6380        let processor = TxProcessor::new(state.clone());
6381        let treasury = Pubkey([3u8; 32]);
6382        state.set_treasury_pubkey(&treasury).unwrap();
6383        state
6384            .put_account(&treasury, &Account::new(0, treasury))
6385            .unwrap();
6386
6387        // Poor alice with only 1 spore
6388        let alice_kp = Keypair::generate();
6389        let alice = alice_kp.pubkey();
6390        let mut poor_account = Account::new(0, alice);
6391        poor_account.spores = 1;
6392        poor_account.spendable = 1;
6393        state.put_account(&alice, &poor_account).unwrap();
6394
6395        let genesis = crate::Block::new_with_timestamp(
6396            0,
6397            Hash::default(),
6398            Hash::default(),
6399            [0u8; 32],
6400            Vec::new(),
6401            0,
6402        );
6403        let genesis_hash = genesis.hash();
6404        state.put_block(&genesis).unwrap();
6405        state.set_last_slot(0).unwrap();
6406
6407        let nonce_pk = Pubkey([99u8; 32]);
6408        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6409        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
6410        let mut tx = Transaction::new(message);
6411        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6412
6413        let validator = Pubkey([42u8; 32]);
6414        let result = processor.process_transaction(&tx, &validator);
6415        assert!(!result.success);
6416    }
6417
6418    #[test]
6419    fn test_nonce_advance() {
6420        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6421        let validator = Pubkey([42u8; 32]);
6422        let nonce_pk = Pubkey([99u8; 32]);
6423
6424        // Step 1: Initialize nonce
6425        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6426        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6427        let mut tx = Transaction::new(msg);
6428        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6429        let r = processor.process_transaction(&tx, &validator);
6430        assert!(r.success, "Init failed: {:?}", r.error);
6431
6432        // Step 2: Advance the nonce — need a new block so blockhash changes
6433        let block1 = crate::Block::new_with_timestamp(
6434            1,
6435            genesis_hash,
6436            Hash::default(),
6437            [0u8; 32],
6438            Vec::new(),
6439            1,
6440        );
6441        let block1_hash = block1.hash();
6442        state.put_block(&block1).unwrap();
6443        state.set_last_slot(1).unwrap();
6444
6445        let advance_ix = make_nonce_advance_ix(alice, nonce_pk);
6446        let msg2 = crate::transaction::Message::new(vec![advance_ix], block1_hash);
6447        let mut tx2 = Transaction::new(msg2);
6448        tx2.signatures.push(alice_kp.sign(&tx2.message.serialize()));
6449        let r2 = processor.process_transaction(&tx2, &validator);
6450        assert!(r2.success, "Advance failed: {:?}", r2.error);
6451
6452        // Verify blockhash updated
6453        let nonce_acct = state.get_account(&nonce_pk).unwrap().unwrap();
6454        let ns = TxProcessor::decode_nonce_state(&nonce_acct.data).unwrap();
6455        assert_eq!(ns.blockhash, block1_hash);
6456    }
6457
6458    #[test]
6459    fn test_nonce_advance_rejects_same_blockhash() {
6460        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6461        let validator = Pubkey([42u8; 32]);
6462        let nonce_pk = Pubkey([99u8; 32]);
6463
6464        // Initialize nonce (stores genesis_hash)
6465        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6466        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6467        let mut tx = Transaction::new(msg);
6468        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6469        assert!(processor.process_transaction(&tx, &validator).success);
6470
6471        // Try to advance without a new block — blockhash hasn't changed
6472        let advance_ix = make_nonce_advance_ix(alice, nonce_pk);
6473        let msg2 = crate::transaction::Message::new(vec![advance_ix], genesis_hash);
6474        let mut tx2 = Transaction::new(msg2);
6475        tx2.signatures.push(alice_kp.sign(&tx2.message.serialize()));
6476        let r = processor.process_transaction(&tx2, &validator);
6477        assert!(!r.success);
6478        assert!(r.error.as_ref().unwrap().contains("has not changed"));
6479    }
6480
6481    #[test]
6482    fn test_durable_tx_with_nonce_blockhash() {
6483        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6484        let validator = Pubkey([42u8; 32]);
6485        let nonce_pk = Pubkey([99u8; 32]);
6486        let bob = Pubkey([2u8; 32]);
6487
6488        // Step 1: Initialize nonce (stores genesis_hash)
6489        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6490        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6491        let mut tx = Transaction::new(msg);
6492        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6493        assert!(processor.process_transaction(&tx, &validator).success);
6494
6495        // Step 2: Create many new blocks to push genesis_hash out of the recent window
6496        let mut prev_hash = genesis_hash;
6497        for slot in 1..=350 {
6498            let block = crate::Block::new_with_timestamp(
6499                slot,
6500                prev_hash,
6501                Hash::default(),
6502                [0u8; 32],
6503                Vec::new(),
6504                slot,
6505            );
6506            prev_hash = block.hash();
6507            state.put_block(&block).unwrap();
6508            state.set_last_slot(slot).unwrap();
6509        }
6510
6511        // Confirm genesis_hash is now too old for a normal tx
6512        let normal_tx = make_transfer_tx(&alice_kp, alice, bob, 1, genesis_hash);
6513        let normal_result = processor.process_transaction(&normal_tx, &validator);
6514        assert!(
6515            !normal_result.success,
6516            "Normal tx with old blockhash should fail"
6517        );
6518        assert!(normal_result
6519            .error
6520            .as_ref()
6521            .unwrap()
6522            .contains("Blockhash not found or too old"));
6523
6524        // Step 3: Build a durable tx using the nonce's stored blockhash (genesis_hash)
6525        // First instruction = AdvanceNonce, second = Transfer
6526        let advance_ix = make_nonce_advance_ix(alice, nonce_pk);
6527        let mut transfer_data = vec![0u8];
6528        transfer_data.extend_from_slice(&Account::licn_to_spores(1).to_le_bytes());
6529        let transfer_ix = Instruction {
6530            program_id: SYSTEM_PROGRAM_ID,
6531            accounts: vec![alice, bob],
6532            data: transfer_data,
6533        };
6534
6535        let msg = crate::transaction::Message::new(vec![advance_ix, transfer_ix], genesis_hash);
6536        let mut durable_tx = Transaction::new(msg);
6537        durable_tx
6538            .signatures
6539            .push(alice_kp.sign(&durable_tx.message.serialize()));
6540
6541        let durable_result = processor.process_transaction(&durable_tx, &validator);
6542        assert!(
6543            durable_result.success,
6544            "Durable nonce tx should succeed: {:?}",
6545            durable_result.error,
6546        );
6547
6548        // Bob should have received 1 LICN
6549        assert_eq!(state.get_balance(&bob).unwrap(), Account::licn_to_spores(1));
6550
6551        // Nonce should be advanced to latest blockhash
6552        let nonce_acct = state.get_account(&nonce_pk).unwrap().unwrap();
6553        let ns = TxProcessor::decode_nonce_state(&nonce_acct.data).unwrap();
6554        assert_eq!(ns.blockhash, prev_hash);
6555    }
6556
6557    #[test]
6558    fn test_nonce_withdraw() {
6559        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6560        let validator = Pubkey([42u8; 32]);
6561        let nonce_pk = Pubkey([99u8; 32]);
6562        let bob = Pubkey([2u8; 32]);
6563
6564        // Initialize nonce
6565        initialize_test_nonce(
6566            &processor,
6567            &alice_kp,
6568            alice,
6569            nonce_pk,
6570            alice,
6571            genesis_hash,
6572            &validator,
6573        );
6574
6575        // Withdraw funds to bob
6576        let withdraw_ix = make_nonce_withdraw_ix(alice, nonce_pk, bob, NONCE_ACCOUNT_MIN_BALANCE);
6577        let tx2 = make_signed_tx(&alice_kp, withdraw_ix, genesis_hash);
6578        let r = processor.process_transaction(&tx2, &validator);
6579        assert!(r.success, "Withdraw failed: {:?}", r.error);
6580
6581        // Bob should have received the nonce balance
6582        let bob_balance = state.get_balance(&bob).unwrap();
6583        assert_eq!(bob_balance, NONCE_ACCOUNT_MIN_BALANCE);
6584
6585        // Nonce account data should be cleared (closed)
6586        let nonce_acct = state.get_account(&nonce_pk).unwrap().unwrap();
6587        assert!(nonce_acct.data.is_empty());
6588    }
6589
6590    #[test]
6591    fn test_nonce_withdraw_authority_restriction_blocks_value_exit() {
6592        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6593        let validator = Pubkey([42u8; 32]);
6594        let nonce_pk = Pubkey([99u8; 32]);
6595        let bob = Pubkey([2u8; 32]);
6596
6597        initialize_test_nonce(
6598            &processor,
6599            &alice_kp,
6600            alice,
6601            nonce_pk,
6602            alice,
6603            genesis_hash,
6604            &validator,
6605        );
6606        let before_nonce_account = state.get_account(&nonce_pk).unwrap().unwrap();
6607
6608        put_active_processor_test_restriction(
6609            &state,
6610            RestrictionTarget::Account(alice),
6611            RestrictionMode::OutgoingOnly,
6612        );
6613
6614        let withdraw_ix = make_nonce_withdraw_ix(alice, nonce_pk, bob, NONCE_ACCOUNT_MIN_BALANCE);
6615        let tx = make_signed_tx(&alice_kp, withdraw_ix, genesis_hash);
6616        let result = processor.process_transaction(&tx, &validator);
6617        assert!(!result.success);
6618        assert_failed_nonce_withdraw_keeps_nonce_open(
6619            &state,
6620            nonce_pk,
6621            bob,
6622            &result.error,
6623            "authority value exit blocked by active account restriction",
6624            &before_nonce_account,
6625        );
6626    }
6627
6628    #[test]
6629    fn test_nonce_withdraw_authority_native_frozen_amount_blocks_value_exit() {
6630        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6631        let validator = Pubkey([42u8; 32]);
6632        let nonce_pk = Pubkey([99u8; 32]);
6633        let bob = Pubkey([2u8; 32]);
6634
6635        initialize_test_nonce(
6636            &processor,
6637            &alice_kp,
6638            alice,
6639            nonce_pk,
6640            alice,
6641            genesis_hash,
6642            &validator,
6643        );
6644        let before_nonce_account = state.get_account(&nonce_pk).unwrap().unwrap();
6645        let authority_spendable = state
6646            .get_account(&alice)
6647            .unwrap()
6648            .expect("authority account should exist")
6649            .spendable;
6650
6651        put_active_processor_test_restriction(
6652            &state,
6653            RestrictionTarget::AccountAsset {
6654                account: alice,
6655                asset: NATIVE_LICN_ASSET_ID,
6656            },
6657            RestrictionMode::FrozenAmount {
6658                amount: authority_spendable,
6659            },
6660        );
6661
6662        let withdraw_ix = make_nonce_withdraw_ix(alice, nonce_pk, bob, NONCE_ACCOUNT_MIN_BALANCE);
6663        let tx = make_signed_tx(&alice_kp, withdraw_ix, genesis_hash);
6664        let result = processor.process_transaction(&tx, &validator);
6665        assert!(!result.success);
6666        assert_failed_nonce_withdraw_keeps_nonce_open(
6667            &state,
6668            nonce_pk,
6669            bob,
6670            &result.error,
6671            "authority value exit blocked by active account-asset restriction",
6672            &before_nonce_account,
6673        );
6674    }
6675
6676    #[test]
6677    fn test_nonce_withdraw_nonce_account_restriction_blocks_value_exit() {
6678        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6679        let validator = Pubkey([42u8; 32]);
6680        let nonce_pk = Pubkey([99u8; 32]);
6681        let bob = Pubkey([2u8; 32]);
6682
6683        initialize_test_nonce(
6684            &processor,
6685            &alice_kp,
6686            alice,
6687            nonce_pk,
6688            alice,
6689            genesis_hash,
6690            &validator,
6691        );
6692        let before_nonce_account = state.get_account(&nonce_pk).unwrap().unwrap();
6693
6694        put_active_processor_test_restriction(
6695            &state,
6696            RestrictionTarget::Account(nonce_pk),
6697            RestrictionMode::OutgoingOnly,
6698        );
6699
6700        let withdraw_ix = make_nonce_withdraw_ix(alice, nonce_pk, bob, NONCE_ACCOUNT_MIN_BALANCE);
6701        let tx = make_signed_tx(&alice_kp, withdraw_ix, genesis_hash);
6702        let result = processor.process_transaction(&tx, &validator);
6703        assert!(!result.success);
6704        assert_failed_nonce_withdraw_keeps_nonce_open(
6705            &state,
6706            nonce_pk,
6707            bob,
6708            &result.error,
6709            "sender account restriction",
6710            &before_nonce_account,
6711        );
6712    }
6713
6714    #[test]
6715    fn test_nonce_withdraw_recipient_restriction_blocks_without_closing_nonce() {
6716        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6717        let validator = Pubkey([42u8; 32]);
6718        let nonce_pk = Pubkey([99u8; 32]);
6719        let bob = Pubkey([2u8; 32]);
6720
6721        initialize_test_nonce(
6722            &processor,
6723            &alice_kp,
6724            alice,
6725            nonce_pk,
6726            alice,
6727            genesis_hash,
6728            &validator,
6729        );
6730        let before_nonce_account = state.get_account(&nonce_pk).unwrap().unwrap();
6731
6732        put_active_processor_test_restriction(
6733            &state,
6734            RestrictionTarget::Account(bob),
6735            RestrictionMode::IncomingOnly,
6736        );
6737
6738        let withdraw_ix = make_nonce_withdraw_ix(alice, nonce_pk, bob, NONCE_ACCOUNT_MIN_BALANCE);
6739        let tx = make_signed_tx(&alice_kp, withdraw_ix, genesis_hash);
6740        let result = processor.process_transaction(&tx, &validator);
6741        assert!(!result.success);
6742        assert_failed_nonce_withdraw_keeps_nonce_open(
6743            &state,
6744            nonce_pk,
6745            bob,
6746            &result.error,
6747            "recipient account restriction",
6748            &before_nonce_account,
6749        );
6750    }
6751
6752    #[test]
6753    fn test_nonce_authorize() {
6754        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6755        let validator = Pubkey([42u8; 32]);
6756        let nonce_pk = Pubkey([99u8; 32]);
6757        let new_auth = Pubkey([77u8; 32]);
6758
6759        // Initialize nonce with alice as authority
6760        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6761        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6762        let mut tx = Transaction::new(msg);
6763        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6764        assert!(processor.process_transaction(&tx, &validator).success);
6765
6766        // Change authority to new_auth
6767        let mut auth_data = vec![28u8, 3u8];
6768        auth_data.extend_from_slice(&new_auth.0);
6769        let auth_ix = Instruction {
6770            program_id: SYSTEM_PROGRAM_ID,
6771            accounts: vec![alice, nonce_pk],
6772            data: auth_data,
6773        };
6774        let msg2 = crate::transaction::Message::new(vec![auth_ix], genesis_hash);
6775        let mut tx2 = Transaction::new(msg2);
6776        tx2.signatures.push(alice_kp.sign(&tx2.message.serialize()));
6777        let r = processor.process_transaction(&tx2, &validator);
6778        assert!(r.success, "Authorize failed: {:?}", r.error);
6779
6780        // Verify authority changed
6781        let nonce_acct = state.get_account(&nonce_pk).unwrap().unwrap();
6782        let ns = TxProcessor::decode_nonce_state(&nonce_acct.data).unwrap();
6783        assert_eq!(ns.authority, new_auth);
6784    }
6785
6786    #[test]
6787    fn test_nonce_authorize_rejects_zero_authority() {
6788        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6789        let validator = Pubkey([42u8; 32]);
6790        let nonce_pk = Pubkey([99u8; 32]);
6791
6792        // Initialize
6793        let ix = make_nonce_init_ix(alice, nonce_pk, alice);
6794        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6795        let mut tx = Transaction::new(msg);
6796        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6797        assert!(processor.process_transaction(&tx, &validator).success);
6798
6799        // Try to set zero authority
6800        let mut auth_data = vec![28u8, 3u8];
6801        auth_data.extend_from_slice(&[0u8; 32]);
6802        let auth_ix = Instruction {
6803            program_id: SYSTEM_PROGRAM_ID,
6804            accounts: vec![alice, nonce_pk],
6805            data: auth_data,
6806        };
6807        let msg2 = crate::transaction::Message::new(vec![auth_ix], genesis_hash);
6808        let mut tx2 = Transaction::new(msg2);
6809        tx2.signatures.push(alice_kp.sign(&tx2.message.serialize()));
6810        let r = processor.process_transaction(&tx2, &validator);
6811        assert!(!r.success);
6812        assert!(r.error.as_ref().unwrap().contains("zero pubkey"));
6813    }
6814
6815    #[test]
6816    fn test_decode_nonce_state_invalid_data() {
6817        // Empty data
6818        assert!(TxProcessor::decode_nonce_state(&[]).is_err());
6819        // Wrong marker
6820        assert!(TxProcessor::decode_nonce_state(&[0x00, 0x01]).is_err());
6821        // Correct marker but garbage
6822        assert!(TxProcessor::decode_nonce_state(&[NONCE_ACCOUNT_MARKER, 0xFF]).is_err());
6823    }
6824
6825    #[test]
6826    fn test_nonce_unknown_sub_opcode() {
6827        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6828        let validator = Pubkey([42u8; 32]);
6829        let nonce_pk = Pubkey([99u8; 32]);
6830
6831        let ix = Instruction {
6832            program_id: SYSTEM_PROGRAM_ID,
6833            accounts: vec![alice, nonce_pk],
6834            data: vec![28u8, 99u8], // unknown sub-opcode
6835        };
6836        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6837        let mut tx = Transaction::new(msg);
6838        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6839        let r = processor.process_transaction(&tx, &validator);
6840        assert!(!r.success);
6841        assert!(r.error.as_ref().unwrap().contains("unknown sub-opcode"));
6842    }
6843
6844    // ── Governance parameter change tests (system instruction type 29) ──
6845
6846    /// Helper: build a governance param change instruction
6847    fn make_gov_param_ix(signer: Pubkey, param_id: u8, value: u64) -> Instruction {
6848        let mut data = vec![29u8, param_id];
6849        data.extend_from_slice(&value.to_le_bytes());
6850        Instruction {
6851            program_id: SYSTEM_PROGRAM_ID,
6852            accounts: vec![signer],
6853            data,
6854        }
6855    }
6856
6857    #[test]
6858    fn test_governance_param_change_base_fee() {
6859        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6860        let validator = Pubkey([42u8; 32]);
6861
6862        // Set alice as governance authority
6863        state.set_governance_authority(&alice).unwrap();
6864
6865        // Change base_fee to 2,000,000 spores (0.002 LICN)
6866        let new_base_fee = 2_000_000u64;
6867        let ix = make_gov_param_ix(alice, GOV_PARAM_BASE_FEE, new_base_fee);
6868        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6869        let mut tx = Transaction::new(msg);
6870        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6871        let r = processor.process_transaction(&tx, &validator);
6872        assert!(r.success, "failed: {:?}", r.error);
6873
6874        // Verify it's queued but not yet applied
6875        let pending = state.get_pending_governance_changes().unwrap();
6876        assert_eq!(pending.len(), 1);
6877        assert_eq!(pending[0], (GOV_PARAM_BASE_FEE, new_base_fee));
6878
6879        // Apply pending changes (simulating epoch boundary)
6880        let applied = state.apply_pending_governance_changes().unwrap();
6881        assert_eq!(applied, 1);
6882
6883        // Verify the fee config was updated
6884        let fee_config = state.get_fee_config().unwrap();
6885        assert_eq!(fee_config.base_fee, new_base_fee);
6886
6887        // Pending changes should be cleared
6888        let pending = state.get_pending_governance_changes().unwrap();
6889        assert!(pending.is_empty());
6890    }
6891
6892    #[test]
6893    fn test_governance_param_change_fee_percentages() {
6894        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6895        let validator = Pubkey([42u8; 32]);
6896
6897        state.set_governance_authority(&alice).unwrap();
6898
6899        // Change burn percent to 50% and producer percent to 20%
6900        let ix1 = make_gov_param_ix(alice, GOV_PARAM_FEE_BURN_PERCENT, 50);
6901        let ix2 = make_gov_param_ix(alice, GOV_PARAM_FEE_PRODUCER_PERCENT, 20);
6902
6903        // Submit both in one tx
6904        let msg = crate::transaction::Message::new(vec![ix1, ix2], genesis_hash);
6905        let mut tx = Transaction::new(msg);
6906        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6907        let r = processor.process_transaction(&tx, &validator);
6908        assert!(r.success, "failed: {:?}", r.error);
6909
6910        let pending = state.get_pending_governance_changes().unwrap();
6911        assert_eq!(pending.len(), 2);
6912
6913        let applied = state.apply_pending_governance_changes().unwrap();
6914        assert_eq!(applied, 2);
6915
6916        let fee_config = state.get_fee_config().unwrap();
6917        assert_eq!(fee_config.fee_burn_percent, 50);
6918        assert_eq!(fee_config.fee_producer_percent, 20);
6919    }
6920
6921    #[test]
6922    fn test_governance_param_change_min_validator_stake() {
6923        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6924        let validator = Pubkey([42u8; 32]);
6925
6926        state.set_governance_authority(&alice).unwrap();
6927
6928        // Change min_validator_stake to 100 LICN
6929        let new_stake = 100_000_000_000u64; // 100 LICN in spores
6930        let ix = make_gov_param_ix(alice, GOV_PARAM_MIN_VALIDATOR_STAKE, new_stake);
6931        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6932        let mut tx = Transaction::new(msg);
6933        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6934        let r = processor.process_transaction(&tx, &validator);
6935        assert!(r.success, "failed: {:?}", r.error);
6936
6937        let applied = state.apply_pending_governance_changes().unwrap();
6938        assert_eq!(applied, 1);
6939
6940        let stored = state.get_min_validator_stake().unwrap();
6941        assert_eq!(stored, Some(new_stake));
6942    }
6943
6944    #[test]
6945    fn test_governance_param_change_epoch_slots() {
6946        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6947        let validator = Pubkey([42u8; 32]);
6948
6949        state.set_governance_authority(&alice).unwrap();
6950
6951        // Change epoch_slots to 100,000
6952        let new_epoch = 100_000u64;
6953        let ix = make_gov_param_ix(alice, GOV_PARAM_EPOCH_SLOTS, new_epoch);
6954        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6955        let mut tx = Transaction::new(msg);
6956        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6957        let r = processor.process_transaction(&tx, &validator);
6958        assert!(r.success, "failed: {:?}", r.error);
6959
6960        let applied = state.apply_pending_governance_changes().unwrap();
6961        assert_eq!(applied, 1);
6962
6963        let stored = state.get_epoch_slots().unwrap();
6964        assert_eq!(stored, Some(new_epoch));
6965    }
6966
6967    #[test]
6968    fn test_governance_param_change_rejects_non_authority() {
6969        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
6970        let validator = Pubkey([42u8; 32]);
6971
6972        // Set a different pubkey as governance authority (not alice)
6973        let gov_auth = Pubkey([77u8; 32]);
6974        state.set_governance_authority(&gov_auth).unwrap();
6975
6976        // Alice tries to submit governance change — should be rejected
6977        let ix = make_gov_param_ix(alice, GOV_PARAM_BASE_FEE, 2_000_000);
6978        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
6979        let mut tx = Transaction::new(msg);
6980        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
6981        let r = processor.process_transaction(&tx, &validator);
6982        assert!(!r.success);
6983        assert!(
6984            r.error
6985                .as_ref()
6986                .unwrap()
6987                .contains("not the governance authority"),
6988            "unexpected: {:?}",
6989            r.error
6990        );
6991    }
6992
6993    #[test]
6994    fn test_governance_param_change_rejects_no_authority_configured() {
6995        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
6996        let validator = Pubkey([42u8; 32]);
6997
6998        // No governance authority configured
6999        let ix = make_gov_param_ix(alice, GOV_PARAM_BASE_FEE, 2_000_000);
7000        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7001        let mut tx = Transaction::new(msg);
7002        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7003        let r = processor.process_transaction(&tx, &validator);
7004        assert!(!r.success);
7005        assert!(
7006            r.error
7007                .as_ref()
7008                .unwrap()
7009                .contains("no governance authority configured"),
7010            "unexpected: {:?}",
7011            r.error
7012        );
7013    }
7014
7015    #[test]
7016    fn test_governance_param_change_rejects_invalid_base_fee() {
7017        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7018        let validator = Pubkey([42u8; 32]);
7019
7020        state.set_governance_authority(&alice).unwrap();
7021
7022        // base_fee = 0 (too low)
7023        let ix = make_gov_param_ix(alice, GOV_PARAM_BASE_FEE, 0);
7024        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7025        let mut tx = Transaction::new(msg);
7026        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7027        let r = processor.process_transaction(&tx, &validator);
7028        assert!(!r.success);
7029        assert!(
7030            r.error.as_ref().unwrap().contains("base_fee must be"),
7031            "unexpected: {:?}",
7032            r.error
7033        );
7034    }
7035
7036    #[test]
7037    fn test_governance_param_change_rejects_invalid_percentage() {
7038        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7039        let validator = Pubkey([42u8; 32]);
7040
7041        state.set_governance_authority(&alice).unwrap();
7042
7043        // fee_burn_percent = 101 (too high)
7044        let ix = make_gov_param_ix(alice, GOV_PARAM_FEE_BURN_PERCENT, 101);
7045        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7046        let mut tx = Transaction::new(msg);
7047        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7048        let r = processor.process_transaction(&tx, &validator);
7049        assert!(!r.success);
7050        assert!(
7051            r.error.as_ref().unwrap().contains("fee percentage must be"),
7052            "unexpected: {:?}",
7053            r.error
7054        );
7055    }
7056
7057    #[test]
7058    fn test_governance_param_change_rejects_fee_split_sum_over_100() {
7059        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7060        let validator = Pubkey([42u8; 32]);
7061
7062        state.set_governance_authority(&alice).unwrap();
7063
7064        let burn = make_gov_param_ix(alice, GOV_PARAM_FEE_BURN_PERCENT, 80);
7065        let producer = make_gov_param_ix(alice, GOV_PARAM_FEE_PRODUCER_PERCENT, 30);
7066        let msg = crate::transaction::Message::new(vec![burn, producer], genesis_hash);
7067        let mut tx = Transaction::new(msg);
7068        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7069
7070        let result = processor.process_transaction(&tx, &validator);
7071        assert!(!result.success, "invalid fee split must be rejected");
7072        assert!(
7073            result
7074                .error
7075                .as_deref()
7076                .unwrap_or_default()
7077                .contains("sum to 100"),
7078            "unexpected: {:?}",
7079            result.error
7080        );
7081    }
7082
7083    #[test]
7084    fn test_governance_param_change_rejects_unknown_param() {
7085        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7086        let validator = Pubkey([42u8; 32]);
7087
7088        state.set_governance_authority(&alice).unwrap();
7089
7090        // param_id = 99 (unknown)
7091        let ix = make_gov_param_ix(alice, 99, 1000);
7092        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7093        let mut tx = Transaction::new(msg);
7094        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7095        let r = processor.process_transaction(&tx, &validator);
7096        assert!(!r.success);
7097        assert!(
7098            r.error.as_ref().unwrap().contains("unknown param_id"),
7099            "unexpected: {:?}",
7100            r.error
7101        );
7102    }
7103
7104    #[test]
7105    fn test_governance_param_change_data_too_short() {
7106        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7107        let validator = Pubkey([42u8; 32]);
7108
7109        state.set_governance_authority(&alice).unwrap();
7110
7111        // Only 2 bytes (no value)
7112        let ix = Instruction {
7113            program_id: SYSTEM_PROGRAM_ID,
7114            accounts: vec![alice],
7115            data: vec![29u8, 0u8],
7116        };
7117        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7118        let mut tx = Transaction::new(msg);
7119        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7120        let r = processor.process_transaction(&tx, &validator);
7121        assert!(!r.success);
7122        assert!(
7123            r.error.as_ref().unwrap().contains("data too short"),
7124            "unexpected: {:?}",
7125            r.error
7126        );
7127    }
7128
7129    #[test]
7130    fn test_governance_param_overwrite_pending() {
7131        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7132        let validator = Pubkey([42u8; 32]);
7133
7134        state.set_governance_authority(&alice).unwrap();
7135
7136        // Queue base_fee = 2M
7137        let ix = make_gov_param_ix(alice, GOV_PARAM_BASE_FEE, 2_000_000);
7138        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7139        let mut tx = Transaction::new(msg);
7140        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7141        let r = processor.process_transaction(&tx, &validator);
7142        assert!(r.success, "failed: {:?}", r.error);
7143
7144        // Overwrite with base_fee = 3M
7145        let ix2 = make_gov_param_ix(alice, GOV_PARAM_BASE_FEE, 3_000_000);
7146        let msg2 = crate::transaction::Message::new(vec![ix2], genesis_hash);
7147        let mut tx2 = Transaction::new(msg2);
7148        tx2.signatures.push(alice_kp.sign(&tx2.message.serialize()));
7149        let r2 = processor.process_transaction(&tx2, &validator);
7150        assert!(r2.success, "failed: {:?}", r2.error);
7151
7152        // Only 1 pending change (overwritten), and it's the latest value
7153        let pending = state.get_pending_governance_changes().unwrap();
7154        assert_eq!(pending.len(), 1);
7155        assert_eq!(pending[0], (GOV_PARAM_BASE_FEE, 3_000_000));
7156    }
7157
7158    #[test]
7159    fn test_governance_param_change_via_governed_authority_proposal_flow() {
7160        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7161        let validator = Pubkey([42u8; 32]);
7162        let bob_kp = Keypair::generate();
7163        let bob = bob_kp.pubkey();
7164        let gov_kp = Keypair::generate();
7165        let gov = gov_kp.pubkey();
7166
7167        let fund = Account::licn_to_spores(1_000);
7168        state
7169            .put_account(&alice, &Account::new(fund, alice))
7170            .unwrap();
7171        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
7172        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
7173        state.set_last_slot(0).unwrap();
7174        state.set_governance_authority(&gov).unwrap();
7175        state
7176            .set_governed_wallet_config(
7177                &gov,
7178                &crate::multisig::GovernedWalletConfig::new(
7179                    2,
7180                    vec![alice, bob, gov],
7181                    "community_treasury",
7182                )
7183                .with_timelock(1),
7184            )
7185            .unwrap();
7186
7187        let direct_ix = make_gov_param_ix(gov, GOV_PARAM_BASE_FEE, 2_000_000);
7188        let direct_msg = crate::transaction::Message::new(vec![direct_ix], genesis_hash);
7189        let mut direct_tx = Transaction::new(direct_msg);
7190        direct_tx
7191            .signatures
7192            .push(gov_kp.sign(&direct_tx.message.serialize()));
7193        let direct_result = processor.process_transaction(&direct_tx, &validator);
7194        assert!(!direct_result.success);
7195        assert!(direct_result
7196            .error
7197            .as_deref()
7198            .unwrap_or("")
7199            .contains("proposal flow"));
7200
7201        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_PARAM_CHANGE, GOV_PARAM_BASE_FEE];
7202        propose_data.extend_from_slice(&2_000_000u64.to_le_bytes());
7203        let propose_ix = Instruction {
7204            program_id: SYSTEM_PROGRAM_ID,
7205            accounts: vec![alice, gov],
7206            data: propose_data,
7207        };
7208        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
7209        let result = processor.process_transaction(&propose_tx, &validator);
7210        assert!(
7211            result.success,
7212            "Proposal should succeed: {:?}",
7213            result.error
7214        );
7215
7216        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
7217        assert_eq!(proposal.action_label, "governance_param_change");
7218        assert_eq!(proposal.approval_authority, None);
7219        assert!(!proposal.executed);
7220        assert_eq!(proposal.execute_after_epoch, 1);
7221
7222        let mut approve_data = vec![35u8];
7223        approve_data.extend_from_slice(&1u64.to_le_bytes());
7224        let approve_ix = Instruction {
7225            program_id: SYSTEM_PROGRAM_ID,
7226            accounts: vec![bob],
7227            data: approve_data,
7228        };
7229        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
7230        let result = processor.process_transaction(&approve_tx, &validator);
7231        assert!(
7232            result.success,
7233            "Approval should succeed: {:?}",
7234            result.error
7235        );
7236        assert!(state.get_pending_governance_changes().unwrap().is_empty());
7237
7238        let mut execute_data = vec![36u8];
7239        execute_data.extend_from_slice(&1u64.to_le_bytes());
7240        let execute_ix = Instruction {
7241            program_id: SYSTEM_PROGRAM_ID,
7242            accounts: vec![alice],
7243            data: execute_data.clone(),
7244        };
7245        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
7246        let result = processor.process_transaction(&execute_tx, &validator);
7247        assert!(!result.success);
7248        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
7249
7250        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
7251
7252        let execute_ix = Instruction {
7253            program_id: SYSTEM_PROGRAM_ID,
7254            accounts: vec![bob],
7255            data: execute_data,
7256        };
7257        let execute_tx = make_signed_tx(&bob_kp, execute_ix, fresh_blockhash);
7258        let result = processor.process_transaction(&execute_tx, &validator);
7259        assert!(
7260            result.success,
7261            "Execution should succeed: {:?}",
7262            result.error
7263        );
7264
7265        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
7266        assert!(proposal.executed);
7267        let pending = state.get_pending_governance_changes().unwrap();
7268        assert_eq!(pending, vec![(GOV_PARAM_BASE_FEE, 2_000_000)]);
7269    }
7270
7271    #[test]
7272    fn test_governance_treasury_transfer_velocity_policy_snapshots_escalation() {
7273        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7274        let validator = Pubkey([42u8; 32]);
7275        let bob_kp = Keypair::generate();
7276        let bob = bob_kp.pubkey();
7277        let carol_kp = Keypair::generate();
7278        let carol = carol_kp.pubkey();
7279        let gov = Pubkey([0x81; 32]);
7280        let recipient = Pubkey([0x82; 32]);
7281
7282        state.put_account(&bob, &Account::new(1_000, bob)).unwrap();
7283        state
7284            .put_account(&carol, &Account::new(1_000, carol))
7285            .unwrap();
7286        state.put_account(&gov, &Account::new(1_000, gov)).unwrap();
7287        state.set_governance_authority(&gov).unwrap();
7288        state
7289            .set_governed_wallet_config(
7290                &gov,
7291                &crate::multisig::GovernedWalletConfig::new(
7292                    1,
7293                    vec![alice, bob, gov],
7294                    "community_treasury",
7295                )
7296                .with_timelock(5)
7297                .with_transfer_velocity_policy(
7298                    crate::multisig::GovernedTransferVelocityPolicy::new(200, 200, 50, 90, 1, 3),
7299                ),
7300            )
7301            .unwrap();
7302        let treasury_authority =
7303            configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob, carol]);
7304
7305        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_TREASURY_TRANSFER];
7306        propose_data.extend_from_slice(&60u64.to_le_bytes());
7307        let propose_ix = Instruction {
7308            program_id: SYSTEM_PROGRAM_ID,
7309            accounts: vec![alice, treasury_authority, recipient],
7310            data: propose_data,
7311        };
7312        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
7313        let result = processor.process_transaction(&propose_tx, &validator);
7314        assert!(result.success, "proposal failed: {:?}", result.error);
7315
7316        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
7317        assert_eq!(proposal.authority, gov);
7318        assert_eq!(proposal.approval_authority, Some(treasury_authority));
7319        assert_eq!(proposal.threshold, 3);
7320        assert_eq!(proposal.execute_after_epoch, 2);
7321        assert_eq!(
7322            proposal.velocity_tier,
7323            crate::multisig::GovernedTransferVelocityTier::Elevated
7324        );
7325        assert_eq!(proposal.daily_cap_spores, 200);
7326        assert!(!proposal.executed);
7327
7328        let mut approve_data = vec![35u8];
7329        approve_data.extend_from_slice(&1u64.to_le_bytes());
7330        let approve_ix = Instruction {
7331            program_id: SYSTEM_PROGRAM_ID,
7332            accounts: vec![bob],
7333            data: approve_data,
7334        };
7335        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
7336        let result = processor.process_transaction(&approve_tx, &validator);
7337        assert!(result.success, "approval failed: {:?}", result.error);
7338        assert!(!state.get_governance_proposal(1).unwrap().unwrap().executed);
7339
7340        let mut final_approve_data = vec![35u8];
7341        final_approve_data.extend_from_slice(&1u64.to_le_bytes());
7342        let final_approve_ix = Instruction {
7343            program_id: SYSTEM_PROGRAM_ID,
7344            accounts: vec![carol],
7345            data: final_approve_data,
7346        };
7347        let final_approve_tx = make_signed_tx(&carol_kp, final_approve_ix, genesis_hash);
7348        let result = processor.process_transaction(&final_approve_tx, &validator);
7349        assert!(result.success, "final approval failed: {:?}", result.error);
7350        assert!(!state.get_governance_proposal(1).unwrap().unwrap().executed);
7351
7352        let mid_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
7353        let mut execute_data = vec![36u8];
7354        execute_data.extend_from_slice(&1u64.to_le_bytes());
7355        let execute_ix = Instruction {
7356            program_id: SYSTEM_PROGRAM_ID,
7357            accounts: vec![alice],
7358            data: execute_data.clone(),
7359        };
7360        let execute_tx = make_signed_tx(&alice_kp, execute_ix, mid_blockhash);
7361        let result = processor.process_transaction(&execute_tx, &validator);
7362        assert!(!result.success);
7363        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
7364
7365        let fresh_blockhash = advance_test_slot(&state, 2 * SLOTS_PER_EPOCH);
7366        let execute_ix = Instruction {
7367            program_id: SYSTEM_PROGRAM_ID,
7368            accounts: vec![alice],
7369            data: execute_data,
7370        };
7371        let execute_tx = make_signed_tx(&alice_kp, execute_ix, fresh_blockhash);
7372        let result = processor.process_transaction(&execute_tx, &validator);
7373        assert!(result.success, "execution failed: {:?}", result.error);
7374        assert_eq!(state.get_balance(&recipient).unwrap(), 60);
7375    }
7376
7377    #[test]
7378    fn test_governance_treasury_daily_cap_defers_until_next_day() {
7379        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7380        let validator = Pubkey([42u8; 32]);
7381        let gov = Pubkey([0x83; 32]);
7382        let first_recipient = Pubkey([0x84; 32]);
7383        let second_recipient = Pubkey([0x85; 32]);
7384        let treasury_authority = crate::multisig::derive_treasury_executor_authority(&gov);
7385
7386        state.put_account(&gov, &Account::new(1_000, gov)).unwrap();
7387        state.set_governance_authority(&gov).unwrap();
7388        state
7389            .set_governed_wallet_config(
7390                &gov,
7391                &crate::multisig::GovernedWalletConfig::new(1, vec![alice], "community_treasury")
7392                    .with_transfer_velocity_policy(
7393                        crate::multisig::GovernedTransferVelocityPolicy::new(200, 100, 0, 0, 0, 0),
7394                    ),
7395            )
7396            .unwrap();
7397        state
7398            .set_treasury_executor_authority(&treasury_authority)
7399            .unwrap();
7400        state
7401            .set_governed_wallet_config(
7402                &treasury_authority,
7403                &crate::multisig::GovernedWalletConfig::new(
7404                    1,
7405                    vec![alice],
7406                    crate::multisig::TREASURY_EXECUTOR_LABEL,
7407                ),
7408            )
7409            .unwrap();
7410
7411        let mut first_propose_data = vec![34u8, GOVERNANCE_ACTION_TREASURY_TRANSFER];
7412        first_propose_data.extend_from_slice(&60u64.to_le_bytes());
7413        let first_propose_ix = Instruction {
7414            program_id: SYSTEM_PROGRAM_ID,
7415            accounts: vec![alice, treasury_authority, first_recipient],
7416            data: first_propose_data,
7417        };
7418        let first_propose_tx = make_signed_tx(&alice_kp, first_propose_ix, genesis_hash);
7419        let result = processor.process_transaction(&first_propose_tx, &validator);
7420        assert!(result.success, "first transfer failed: {:?}", result.error);
7421        assert!(state.get_governance_proposal(1).unwrap().unwrap().executed);
7422
7423        let mut second_propose_data = vec![34u8, GOVERNANCE_ACTION_TREASURY_TRANSFER];
7424        second_propose_data.extend_from_slice(&50u64.to_le_bytes());
7425        let second_propose_ix = Instruction {
7426            program_id: SYSTEM_PROGRAM_ID,
7427            accounts: vec![alice, treasury_authority, second_recipient],
7428            data: second_propose_data,
7429        };
7430        let second_propose_tx = make_signed_tx(&alice_kp, second_propose_ix, genesis_hash);
7431        let result = processor.process_transaction(&second_propose_tx, &validator);
7432        assert!(result.success, "second proposal failed: {:?}", result.error);
7433
7434        let second_proposal = state.get_governance_proposal(2).unwrap().unwrap();
7435        assert!(!second_proposal.executed);
7436        assert_eq!(state.get_balance(&second_recipient).unwrap(), 0);
7437        assert_eq!(state.get_governed_transfer_day_volume(&gov, 0).unwrap(), 60);
7438
7439        let mut execute_data = vec![36u8];
7440        execute_data.extend_from_slice(&2u64.to_le_bytes());
7441        let execute_ix = Instruction {
7442            program_id: SYSTEM_PROGRAM_ID,
7443            accounts: vec![alice],
7444            data: execute_data.clone(),
7445        };
7446        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
7447        let result = processor.process_transaction(&execute_tx, &validator);
7448        assert!(!result.success);
7449        assert!(result.error.as_deref().unwrap_or("").contains("daily cap"));
7450
7451        let fresh_blockhash = advance_test_slot(&state, SECONDS_PER_DAY);
7452        let execute_ix = Instruction {
7453            program_id: SYSTEM_PROGRAM_ID,
7454            accounts: vec![alice],
7455            data: execute_data,
7456        };
7457        let execute_tx = make_signed_tx(&alice_kp, execute_ix, fresh_blockhash);
7458        let result = processor.process_transaction(&execute_tx, &validator);
7459        assert!(
7460            result.success,
7461            "deferred execute failed: {:?}",
7462            result.error
7463        );
7464        assert!(state.get_governance_proposal(2).unwrap().unwrap().executed);
7465        assert_eq!(state.get_balance(&second_recipient).unwrap(), 50);
7466        assert_eq!(state.get_governed_transfer_day_volume(&gov, 1).unwrap(), 50);
7467    }
7468
7469    #[test]
7470    fn test_governance_treasury_transfer_rejects_general_governance_authority_when_split_is_configured(
7471    ) {
7472        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7473        let validator = Pubkey([42u8; 32]);
7474        let bob = Pubkey([0xA7; 32]);
7475        let gov = Pubkey([0xA8; 32]);
7476        let recipient = Pubkey([0xA9; 32]);
7477
7478        let fund = Account::licn_to_spores(1_000);
7479        state
7480            .put_account(&alice, &Account::new(fund, alice))
7481            .unwrap();
7482        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
7483        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
7484        state.set_governance_authority(&gov).unwrap();
7485        state
7486            .set_governed_wallet_config(
7487                &gov,
7488                &crate::multisig::GovernedWalletConfig::new(
7489                    2,
7490                    vec![alice, bob, gov],
7491                    "community_treasury",
7492                )
7493                .with_transfer_velocity_policy(
7494                    crate::multisig::GovernedTransferVelocityPolicy::community_treasury_defaults(),
7495                ),
7496            )
7497            .unwrap();
7498        configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob]);
7499
7500        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_TREASURY_TRANSFER];
7501        propose_data.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
7502        let propose_ix = Instruction {
7503            program_id: SYSTEM_PROGRAM_ID,
7504            accounts: vec![alice, gov, recipient],
7505            data: propose_data,
7506        };
7507        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
7508        let result = processor.process_transaction(&propose_tx, &validator);
7509        assert!(!result.success);
7510        assert!(result.error.as_deref().unwrap_or("").contains(
7511            "Protocol fund movement governance actions must use the treasury executor approval authority"
7512        ));
7513    }
7514
7515    // ──────────────────────────────────────────────────────────────
7516    // Compute-unit metering tests (Task 2.12)
7517    // ──────────────────────────────────────────────────────────────
7518
7519    #[test]
7520    fn test_cu_lookup_transfer() {
7521        assert_eq!(compute_units_for_system_ix(0), CU_TRANSFER);
7522        // Multi-transfer variants (types 2-5) should match
7523        for t in 2..=5u8 {
7524            assert_eq!(compute_units_for_system_ix(t), CU_TRANSFER);
7525        }
7526    }
7527
7528    #[test]
7529    fn test_cu_lookup_stake_unstake() {
7530        assert_eq!(compute_units_for_system_ix(9), CU_STAKE);
7531        assert_eq!(compute_units_for_system_ix(10), CU_UNSTAKE);
7532        assert_eq!(compute_units_for_system_ix(11), CU_CLAIM_UNSTAKE);
7533    }
7534
7535    #[test]
7536    fn test_cu_lookup_nft() {
7537        assert_eq!(compute_units_for_system_ix(7), CU_MINT_NFT);
7538        assert_eq!(compute_units_for_system_ix(8), CU_TRANSFER_NFT);
7539    }
7540
7541    #[test]
7542    fn test_cu_lookup_zk() {
7543        assert_eq!(compute_units_for_system_ix(23), CU_ZK_SHIELD);
7544        assert_eq!(compute_units_for_system_ix(24), CU_ZK_TRANSFER);
7545        assert_eq!(compute_units_for_system_ix(25), CU_ZK_TRANSFER);
7546    }
7547
7548    #[test]
7549    fn test_cu_lookup_deploy_contract() {
7550        assert_eq!(compute_units_for_system_ix(17), CU_DEPLOY_CONTRACT);
7551    }
7552
7553    #[test]
7554    fn test_cu_lookup_governance() {
7555        assert_eq!(compute_units_for_system_ix(29), CU_GOVERNANCE_PARAM);
7556    }
7557
7558    #[test]
7559    fn test_cu_lookup_unknown_defaults_to_100() {
7560        assert_eq!(compute_units_for_system_ix(200), 100);
7561        assert_eq!(compute_units_for_system_ix(255), 100);
7562    }
7563
7564    #[test]
7565    fn test_cu_for_tx_single_transfer() {
7566        let ix = Instruction {
7567            program_id: SYSTEM_PROGRAM_ID,
7568            accounts: vec![Pubkey([1; 32]), Pubkey([2; 32])],
7569            data: vec![0u8, 0, 0, 0, 0, 0, 0, 0, 0], // type 0 = transfer
7570        };
7571        let msg = crate::transaction::Message::new(vec![ix], Hash::default());
7572        let tx = Transaction::new(msg);
7573        assert_eq!(compute_units_for_tx(&tx), CU_TRANSFER);
7574    }
7575
7576    #[test]
7577    fn test_cu_for_tx_multi_ix_sums() {
7578        let ix_transfer = Instruction {
7579            program_id: SYSTEM_PROGRAM_ID,
7580            accounts: vec![Pubkey([1; 32]), Pubkey([2; 32])],
7581            data: vec![0u8, 0, 0, 0, 0, 0, 0, 0, 0],
7582        };
7583        let ix_stake = Instruction {
7584            program_id: SYSTEM_PROGRAM_ID,
7585            accounts: vec![Pubkey([1; 32])],
7586            data: vec![9u8, 0, 0, 0, 0, 0, 0, 0, 0],
7587        };
7588        let msg = crate::transaction::Message::new(vec![ix_transfer, ix_stake], Hash::default());
7589        let tx = Transaction::new(msg);
7590        assert_eq!(compute_units_for_tx(&tx), CU_TRANSFER + CU_STAKE);
7591    }
7592
7593    #[test]
7594    fn test_cu_for_tx_ignores_contract_ix() {
7595        let ix_system = Instruction {
7596            program_id: SYSTEM_PROGRAM_ID,
7597            accounts: vec![Pubkey([1; 32]), Pubkey([2; 32])],
7598            data: vec![0u8, 0, 0, 0, 0, 0, 0, 0, 0],
7599        };
7600        let ix_contract = Instruction {
7601            program_id: Pubkey([0xFF; 32]), // CONTRACT_PROGRAM_ID
7602            accounts: vec![Pubkey([3; 32])],
7603            data: vec![1, 2, 3],
7604        };
7605        let msg = crate::transaction::Message::new(vec![ix_system, ix_contract], Hash::default());
7606        let tx = Transaction::new(msg);
7607        // Only the system instruction counts — contract CU is tracked by WASM runtime
7608        assert_eq!(compute_units_for_tx(&tx), CU_TRANSFER);
7609    }
7610
7611    #[test]
7612    fn test_tx_result_has_compute_units_after_transfer() {
7613        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
7614        let bob = Pubkey([2u8; 32]);
7615        let validator = Pubkey([42u8; 32]);
7616
7617        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
7618        let result = processor.process_transaction(&tx, &validator);
7619
7620        assert!(
7621            result.success,
7622            "transfer should succeed: {:?}",
7623            result.error
7624        );
7625        assert_eq!(result.compute_units_used, CU_TRANSFER);
7626    }
7627
7628    // ────────────────────────────────────────────────────────────────────────
7629    // Task 3.6 — Oracle Multi-Source Attestation Tests
7630    // ────────────────────────────────────────────────────────────────────────
7631
7632    /// Helper: build an oracle attestation instruction
7633    fn make_oracle_attestation_ix(
7634        signer: Pubkey,
7635        asset: &str,
7636        price: u64,
7637        decimals: u8,
7638    ) -> Instruction {
7639        let asset_bytes = asset.as_bytes();
7640        let mut data = vec![30u8, asset_bytes.len() as u8];
7641        data.extend_from_slice(asset_bytes);
7642        data.extend_from_slice(&price.to_le_bytes());
7643        data.push(decimals);
7644        Instruction {
7645            program_id: SYSTEM_PROGRAM_ID,
7646            accounts: vec![signer],
7647            data,
7648        }
7649    }
7650
7651    /// Helper: set up a validator with active stake in the stake pool
7652    fn setup_active_validator(state: &StateStore, pubkey: &Pubkey, stake_spores: u64) {
7653        let mut pool = state
7654            .get_stake_pool()
7655            .unwrap_or_else(|_| crate::consensus::StakePool::new());
7656        // Use stake() which requires >= MIN_VALIDATOR_STAKE
7657        pool.stake(*pubkey, stake_spores, 0).unwrap();
7658        state.put_stake_pool(&pool).unwrap();
7659    }
7660
7661    #[test]
7662    fn test_oracle_attestation_basic_submit() {
7663        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7664        let validator = Pubkey([42u8; 32]);
7665
7666        // Make alice an active validator
7667        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7668
7669        // Submit price attestation: LICN = 1.50 (150_000_000 at 8 decimals)
7670        let ix = make_oracle_attestation_ix(alice, "LICN", 150_000_000, 8);
7671        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7672        let mut tx = Transaction::new(msg);
7673        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7674        let r = processor.process_transaction(&tx, &validator);
7675        assert!(r.success, "Attestation should succeed: {:?}", r.error);
7676
7677        // Verify attestation was stored
7678        let attestations = state
7679            .get_oracle_attestations("LICN", 0, ORACLE_STALENESS_SLOTS)
7680            .unwrap();
7681        assert_eq!(attestations.len(), 1);
7682        assert_eq!(attestations[0].price, 150_000_000);
7683        assert_eq!(attestations[0].decimals, 8);
7684        assert_eq!(attestations[0].validator, alice);
7685    }
7686
7687    #[test]
7688    fn test_oracle_attestation_rejects_non_validator() {
7689        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
7690        let validator = Pubkey([42u8; 32]);
7691
7692        // Alice is NOT a validator (no stake)
7693        let ix = make_oracle_attestation_ix(alice, "LICN", 150_000_000, 8);
7694        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7695        let mut tx = Transaction::new(msg);
7696        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7697        let r = processor.process_transaction(&tx, &validator);
7698        assert!(!r.success);
7699        assert!(
7700            r.error.as_ref().unwrap().contains("no stake"),
7701            "unexpected: {:?}",
7702            r.error
7703        );
7704    }
7705
7706    #[test]
7707    fn test_oracle_attestation_rejects_zero_price() {
7708        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7709        let validator = Pubkey([42u8; 32]);
7710        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7711
7712        let ix = make_oracle_attestation_ix(alice, "LICN", 0, 8);
7713        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7714        let mut tx = Transaction::new(msg);
7715        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7716        let r = processor.process_transaction(&tx, &validator);
7717        assert!(!r.success);
7718        assert!(
7719            r.error.as_ref().unwrap().contains("price must be > 0"),
7720            "unexpected: {:?}",
7721            r.error
7722        );
7723    }
7724
7725    #[test]
7726    fn test_oracle_attestation_rejects_invalid_decimals() {
7727        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7728        let validator = Pubkey([42u8; 32]);
7729        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7730
7731        let ix = make_oracle_attestation_ix(alice, "LICN", 100, 19);
7732        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7733        let mut tx = Transaction::new(msg);
7734        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7735        let r = processor.process_transaction(&tx, &validator);
7736        assert!(!r.success);
7737        assert!(
7738            r.error.as_ref().unwrap().contains("decimals must be"),
7739            "unexpected: {:?}",
7740            r.error
7741        );
7742    }
7743
7744    #[test]
7745    fn test_oracle_attestation_rejects_empty_asset() {
7746        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7747        let validator = Pubkey([42u8; 32]);
7748        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7749
7750        // Build manually with asset_len = 0
7751        let mut data = vec![30u8, 0u8]; // asset_len = 0
7752        data.extend_from_slice(&100u64.to_le_bytes());
7753        data.push(8);
7754        let ix = Instruction {
7755            program_id: SYSTEM_PROGRAM_ID,
7756            accounts: vec![alice],
7757            data,
7758        };
7759        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7760        let mut tx = Transaction::new(msg);
7761        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7762        let r = processor.process_transaction(&tx, &validator);
7763        assert!(!r.success);
7764        assert!(
7765            r.error.as_ref().unwrap().contains("asset name length"),
7766            "unexpected: {:?}",
7767            r.error
7768        );
7769    }
7770
7771    #[test]
7772    fn test_oracle_attestation_rejects_too_long_asset() {
7773        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7774        let validator = Pubkey([42u8; 32]);
7775        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7776
7777        // Asset name = 17 bytes (over max 16)
7778        let long_asset = "ABCDEFGHIJKLMNOPQ"; // 17 chars
7779        let ix = make_oracle_attestation_ix(alice, long_asset, 100, 8);
7780        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7781        let mut tx = Transaction::new(msg);
7782        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7783        let r = processor.process_transaction(&tx, &validator);
7784        assert!(!r.success);
7785        assert!(
7786            r.error.as_ref().unwrap().contains("asset name length"),
7787            "unexpected: {:?}",
7788            r.error
7789        );
7790    }
7791
7792    #[test]
7793    fn test_oracle_attestation_data_too_short() {
7794        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7795        let validator = Pubkey([42u8; 32]);
7796        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7797
7798        // Only 3 bytes (opcode + asset_len + 1 byte of asset, missing price + decimals)
7799        let ix = Instruction {
7800            program_id: SYSTEM_PROGRAM_ID,
7801            accounts: vec![alice],
7802            data: vec![30u8, 4u8, b'M', b'O'],
7803        };
7804        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7805        let mut tx = Transaction::new(msg);
7806        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7807        let r = processor.process_transaction(&tx, &validator);
7808        assert!(!r.success);
7809        assert!(
7810            r.error.as_ref().unwrap().contains("data too short"),
7811            "unexpected: {:?}",
7812            r.error
7813        );
7814    }
7815
7816    #[test]
7817    fn test_oracle_quorum_consensus_price() {
7818        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7819
7820        // Three validators with UNEQUAL stakes to test 2/3 threshold boundary.
7821        // Alice: 1 MIN, Bob: 1 MIN, Carol: 4 MIN → total = 6 MIN, threshold = 4 MIN.
7822        // Alice alone (1 MIN) < threshold (4 MIN) → no quorum.
7823        // Alice + Bob (2 MIN) < threshold (4 MIN) → still no quorum.
7824        // Alice + Bob + Carol (6 MIN) >= threshold → quorum (and >= 2 attestors).
7825        let bob_kp = Keypair::generate();
7826        let bob = bob_kp.pubkey();
7827        let carol_kp = Keypair::generate();
7828        let carol = carol_kp.pubkey();
7829
7830        // Fund bob and carol
7831        state.put_account(&bob, &Account::new(1000, bob)).unwrap();
7832        state
7833            .put_account(&carol, &Account::new(1000, carol))
7834            .unwrap();
7835
7836        let stake = MIN_VALIDATOR_STAKE;
7837        {
7838            let mut pool = crate::consensus::StakePool::new();
7839            pool.stake(alice, stake, 0).unwrap();
7840            pool.stake(bob, stake, 0).unwrap();
7841            pool.stake(carol, stake * 4, 0).unwrap();
7842            state.put_stake_pool(&pool).unwrap();
7843        }
7844        // total = 6*stake, threshold = 6*stake*2/3 = 4*stake
7845
7846        let block_producer = Pubkey([42u8; 32]);
7847
7848        // Alice attests: LICN = 150 (stake = 1 MIN < threshold 4 MIN → no quorum)
7849        let ix = make_oracle_attestation_ix(alice, "LICN", 150, 8);
7850        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7851        let mut tx = Transaction::new(msg);
7852        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7853        let r = processor.process_transaction(&tx, &block_producer);
7854        assert!(r.success, "Alice attestation failed: {:?}", r.error);
7855
7856        // 1 MIN < 4 MIN threshold → no consensus
7857        let cp = state.get_oracle_consensus_price("LICN").unwrap();
7858        assert!(
7859            cp.is_none(),
7860            "Should NOT have consensus below 2/3 threshold"
7861        );
7862
7863        // Bob attests: LICN = 160 (combined stake = 2 MIN < threshold 4 MIN → still no quorum)
7864        let ix = make_oracle_attestation_ix(bob, "LICN", 160, 8);
7865        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7866        let mut tx = Transaction::new(msg);
7867        tx.signatures.push(bob_kp.sign(&tx.message.serialize()));
7868        let r = processor.process_transaction(&tx, &block_producer);
7869        assert!(r.success, "Bob attestation failed: {:?}", r.error);
7870
7871        // 2 MIN < 4 MIN threshold → still no consensus with only 2 small validators
7872        let cp = state.get_oracle_consensus_price("LICN").unwrap();
7873        assert!(
7874            cp.is_none(),
7875            "Should NOT have consensus below 2/3 threshold (2 of 6 stake)"
7876        );
7877
7878        // Carol attests: LICN = 155 (combined stake = 6 MIN >= threshold 4 MIN → quorum)
7879        let ix = make_oracle_attestation_ix(carol, "LICN", 155, 8);
7880        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7881        let mut tx = Transaction::new(msg);
7882        tx.signatures.push(carol_kp.sign(&tx.message.serialize()));
7883        let r = processor.process_transaction(&tx, &block_producer);
7884        assert!(r.success, "Carol attestation failed: {:?}", r.error);
7885
7886        // 6 MIN >= 4 MIN threshold, 3 attestors >= 2 → consensus reached
7887        let cp = state.get_oracle_consensus_price("LICN").unwrap();
7888        assert!(cp.is_some(), "Should have consensus with all validators");
7889        let cp = cp.unwrap();
7890        assert_eq!(cp.attestation_count, 3);
7891        // Sorted: [150 (1 MIN), 155 (4 MIN), 160 (1 MIN)].
7892        // Total stake = 6 MIN, half = 3 MIN.
7893        // Cumulative: 150→1 MIN (<3), 155→5 MIN (>=3) → median = 155
7894        assert_eq!(
7895            cp.price, 155,
7896            "Stake-weighted median of [150,155,160] with unequal stakes"
7897        );
7898    }
7899
7900    #[test]
7901    fn test_parallel_oracle_attestations_are_scheduled_sequentially() {
7902        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7903        let bob_kp = Keypair::generate();
7904        let bob = bob_kp.pubkey();
7905        let carol_kp = Keypair::generate();
7906        let carol = carol_kp.pubkey();
7907        let block_producer = Pubkey([42u8; 32]);
7908
7909        state
7910            .put_account(&bob, &Account::new(1_000_000_000_000, bob))
7911            .unwrap();
7912        state
7913            .put_account(&carol, &Account::new(1_000_000_000_000, carol))
7914            .unwrap();
7915
7916        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7917        setup_active_validator(&state, &bob, MIN_VALIDATOR_STAKE);
7918        setup_active_validator(&state, &carol, MIN_VALIDATOR_STAKE);
7919
7920        let mut txs = Vec::new();
7921        for (kp, signer, price) in [
7922            (&alice_kp, alice, 100u64),
7923            (&bob_kp, bob, 200u64),
7924            (&carol_kp, carol, 300u64),
7925        ] {
7926            let ix = make_oracle_attestation_ix(signer, "LICN", price, 8);
7927            let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7928            let mut tx = Transaction::new(msg);
7929            tx.signatures.push(kp.sign(&tx.message.serialize()));
7930            txs.push(tx);
7931        }
7932
7933        let results = processor.process_transactions_parallel(&txs, &block_producer);
7934        for (idx, result) in results.iter().enumerate() {
7935            assert!(
7936                result.success,
7937                "oracle attestation tx {} failed: {:?}",
7938                idx, result.error
7939            );
7940        }
7941
7942        let consensus = state
7943            .get_oracle_consensus_price("LICN")
7944            .unwrap()
7945            .expect("oracle quorum should be reached after three attestations");
7946        assert_eq!(consensus.price, 200);
7947        assert_eq!(consensus.attestation_count, 3);
7948    }
7949
7950    #[test]
7951    fn test_oracle_validator_replaces_own_attestation() {
7952        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7953        let validator = Pubkey([42u8; 32]);
7954        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7955
7956        // First attestation: price = 100
7957        let ix = make_oracle_attestation_ix(alice, "LICN", 100, 8);
7958        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7959        let mut tx = Transaction::new(msg);
7960        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7961        let r = processor.process_transaction(&tx, &validator);
7962        assert!(r.success, "first: {:?}", r.error);
7963
7964        // Second attestation: price = 200 (should replace)
7965        let ix = make_oracle_attestation_ix(alice, "LICN", 200, 8);
7966        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7967        let mut tx = Transaction::new(msg);
7968        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7969        let r = processor.process_transaction(&tx, &validator);
7970        assert!(r.success, "second: {:?}", r.error);
7971
7972        // Should only have 1 attestation (replaced, not appended)
7973        let atts = state
7974            .get_oracle_attestations("LICN", 0, ORACLE_STALENESS_SLOTS)
7975            .unwrap();
7976        assert_eq!(atts.len(), 1);
7977        assert_eq!(atts[0].price, 200);
7978    }
7979
7980    #[test]
7981    fn test_oracle_multi_asset_independence() {
7982        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
7983        let validator = Pubkey([42u8; 32]);
7984        setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7985
7986        // Attest LICN
7987        let ix = make_oracle_attestation_ix(alice, "LICN", 150, 8);
7988        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7989        let mut tx = Transaction::new(msg);
7990        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7991        let r = processor.process_transaction(&tx, &validator);
7992        assert!(r.success, "LICN: {:?}", r.error);
7993
7994        // Attest wETH
7995        let ix = make_oracle_attestation_ix(alice, "wETH", 345_000, 8);
7996        let msg = crate::transaction::Message::new(vec![ix], genesis_hash);
7997        let mut tx = Transaction::new(msg);
7998        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
7999        let r = processor.process_transaction(&tx, &validator);
8000        assert!(r.success, "wETH: {:?}", r.error);
8001
8002        // Check each asset independently
8003        let licn_atts = state
8004            .get_oracle_attestations("LICN", 0, ORACLE_STALENESS_SLOTS)
8005            .unwrap();
8006        let weth_atts = state
8007            .get_oracle_attestations("wETH", 0, ORACLE_STALENESS_SLOTS)
8008            .unwrap();
8009        assert_eq!(licn_atts.len(), 1);
8010        assert_eq!(weth_atts.len(), 1);
8011        assert_eq!(licn_atts[0].price, 150);
8012        assert_eq!(weth_atts[0].price, 345_000);
8013    }
8014
8015    #[test]
8016    fn test_oracle_compute_units() {
8017        assert_eq!(compute_units_for_system_ix(30), CU_ORACLE_ATTESTATION);
8018    }
8019
8020    #[test]
8021    fn test_stake_weighted_median_single() {
8022        let atts = vec![OracleAttestation {
8023            validator: Pubkey([1u8; 32]),
8024            price: 100,
8025            decimals: 8,
8026            stake: 1000,
8027            slot: 0,
8028        }];
8029        assert_eq!(compute_stake_weighted_median(&atts), 100);
8030    }
8031
8032    #[test]
8033    fn test_stake_weighted_median_equal_stakes() {
8034        let atts = vec![
8035            OracleAttestation {
8036                validator: Pubkey([1u8; 32]),
8037                price: 100,
8038                decimals: 8,
8039                stake: 1000,
8040                slot: 0,
8041            },
8042            OracleAttestation {
8043                validator: Pubkey([2u8; 32]),
8044                price: 200,
8045                decimals: 8,
8046                stake: 1000,
8047                slot: 0,
8048            },
8049            OracleAttestation {
8050                validator: Pubkey([3u8; 32]),
8051                price: 300,
8052                decimals: 8,
8053                stake: 1000,
8054                slot: 0,
8055            },
8056        ];
8057        // Sorted: [100, 200, 300], total=3000, half=1500
8058        // Cumulative: 1000, 2000, 3000 → crosses at 200
8059        assert_eq!(compute_stake_weighted_median(&atts), 200);
8060    }
8061
8062    #[test]
8063    fn test_stake_weighted_median_unequal_stakes() {
8064        let atts = vec![
8065            OracleAttestation {
8066                validator: Pubkey([1u8; 32]),
8067                price: 100,
8068                decimals: 8,
8069                stake: 100,
8070                slot: 0,
8071            },
8072            OracleAttestation {
8073                validator: Pubkey([2u8; 32]),
8074                price: 200,
8075                decimals: 8,
8076                stake: 100,
8077                slot: 0,
8078            },
8079            OracleAttestation {
8080                validator: Pubkey([3u8; 32]),
8081                price: 300,
8082                decimals: 8,
8083                stake: 800,
8084                slot: 0,
8085            },
8086        ];
8087        // Sorted: [100, 200, 300], total=1000, half=500
8088        // Cumulative: 100, 200, 1000 → crosses at 300 (the whale's price dominates)
8089        assert_eq!(compute_stake_weighted_median(&atts), 300);
8090    }
8091
8092    #[test]
8093    fn test_stake_weighted_median_empty() {
8094        let atts: Vec<OracleAttestation> = vec![];
8095        assert_eq!(compute_stake_weighted_median(&atts), 0);
8096    }
8097
8098    // ────────────────────────────────────────────────────────────────────────
8099    // Task 3.3 — Contract Upgrade Timelock Tests
8100    // ────────────────────────────────────────────────────────────────────────
8101
8102    /// Helper: deploy a minimal WASM contract and return the contract address and loaded ContractAccount.
8103    fn deploy_test_contract_with_code(
8104        processor: &TxProcessor,
8105        state: &StateStore,
8106        deployer_kp: &crate::Keypair,
8107        deployer: Pubkey,
8108        code: Vec<u8>,
8109        genesis_hash: Hash,
8110        validator: &Pubkey,
8111    ) -> Pubkey {
8112        let code_hash = Hash::hash(&code);
8113        let mut addr_bytes = [0u8; 32];
8114        addr_bytes[..16].copy_from_slice(&deployer.0[..16]);
8115        addr_bytes[16..].copy_from_slice(&code_hash.0[..16]);
8116        let contract_addr = Pubkey(addr_bytes);
8117
8118        let contract_ix = crate::ContractInstruction::Deploy {
8119            code,
8120            init_data: Vec::new(),
8121        };
8122        let ix = Instruction {
8123            program_id: CONTRACT_PROGRAM_ID,
8124            accounts: vec![deployer, contract_addr],
8125            data: contract_ix.serialize().unwrap(),
8126        };
8127        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
8128        let mut tx = Transaction::new(message);
8129        tx.signatures
8130            .push(deployer_kp.sign(&tx.message.serialize()));
8131        let result = processor.process_transaction(&tx, validator);
8132        assert!(result.success, "deploy should succeed: {:?}", result.error);
8133
8134        let acct = state.get_account(&contract_addr).unwrap();
8135        assert!(acct.is_some() && acct.unwrap().executable);
8136        contract_addr
8137    }
8138
8139    fn deploy_test_contract(
8140        processor: &TxProcessor,
8141        state: &StateStore,
8142        deployer_kp: &crate::Keypair,
8143        deployer: Pubkey,
8144        genesis_hash: Hash,
8145        validator: &Pubkey,
8146    ) -> Pubkey {
8147        deploy_test_contract_with_code(
8148            processor,
8149            state,
8150            deployer_kp,
8151            deployer,
8152            vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00],
8153            genesis_hash,
8154            validator,
8155        )
8156    }
8157
8158    fn install_test_contract_account(state: &StateStore, owner: Pubkey, code: Vec<u8>) -> Pubkey {
8159        let code_hash = Hash::hash(&code);
8160        let mut addr_bytes = [0u8; 32];
8161        addr_bytes[..16].copy_from_slice(&owner.0[..16]);
8162        addr_bytes[16..].copy_from_slice(&code_hash.0[..16]);
8163        let contract_addr = Pubkey(addr_bytes);
8164
8165        let contract = crate::ContractAccount::new(code, owner);
8166        let mut account = Account::new(0, contract_addr);
8167        account.data = serde_json::to_vec(&contract).unwrap();
8168        account.executable = true;
8169        state.put_account(&contract_addr, &account).unwrap();
8170        contract_addr
8171    }
8172
8173    fn set_contract_lifecycle_status_for_test(
8174        state: &StateStore,
8175        contract_addr: Pubkey,
8176        status: crate::ContractLifecycleStatus,
8177    ) {
8178        let mut account = state.get_account(&contract_addr).unwrap().unwrap();
8179        let mut contract: crate::ContractAccount = serde_json::from_slice(&account.data).unwrap();
8180        contract.lifecycle_status = status;
8181        contract.lifecycle_updated_slot = 99;
8182        contract.lifecycle_restriction_id = Some(7);
8183        account.data = serde_json::to_vec(&contract).unwrap();
8184        state.put_account(&contract_addr, &account).unwrap();
8185    }
8186
8187    fn load_contract_account_for_test(
8188        state: &StateStore,
8189        contract_addr: Pubkey,
8190    ) -> crate::ContractAccount {
8191        let account = state.get_account(&contract_addr).unwrap().unwrap();
8192        serde_json::from_slice(&account.data).unwrap()
8193    }
8194
8195    /// Helper: build and submit a contract instruction tx.
8196    fn submit_contract_ix(
8197        processor: &TxProcessor,
8198        signer_kp: &crate::Keypair,
8199        accounts: Vec<Pubkey>,
8200        contract_ix: crate::ContractInstruction,
8201        genesis_hash: Hash,
8202        validator: &Pubkey,
8203    ) -> crate::TxResult {
8204        let ix = Instruction {
8205            program_id: CONTRACT_PROGRAM_ID,
8206            accounts,
8207            data: contract_ix.serialize().unwrap(),
8208        };
8209        let message = crate::transaction::Message::new(vec![ix], genesis_hash);
8210        let mut tx = Transaction::new(message);
8211        tx.signatures.push(signer_kp.sign(&tx.message.serialize()));
8212        processor.process_transaction(&tx, validator)
8213    }
8214
8215    /// Helper: build a valid minimal WASM module distinct from the base module.
8216    /// Appends a custom section with the given tag byte so each call produces a
8217    /// different (but valid) WASM binary.
8218    fn valid_wasm_code(tag: u8) -> Vec<u8> {
8219        // magic + version + custom section (id=0, payload_len=2, name_len=1, name=tag)
8220        vec![
8221            0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, tag,
8222        ]
8223    }
8224
8225    fn governance_test_contract_code() -> Vec<u8> {
8226        wat::parse_str(
8227            r#"(module
8228                (import "env" "storage_write" (func $storage_write (param i32 i32 i32 i32) (result i32)))
8229                (import "env" "get_caller" (func $get_caller (param i32) (result i32)))
8230                (import "env" "get_args_len" (func $get_args_len (result i32)))
8231                (import "env" "get_args" (func $get_args (param i32 i32) (result i32)))
8232                (memory (export "memory") 1)
8233                (data (i32.const 0) "last_caller")
8234                (data (i32.const 16) "last_args")
8235                (func $record_call_impl
8236                    (local $args_len i32)
8237                    (drop (call $get_caller (i32.const 64)))
8238                    (drop (call $storage_write (i32.const 0) (i32.const 11) (i32.const 64) (i32.const 32)))
8239                    (local.set $args_len (call $get_args_len))
8240                    (drop (call $get_args (i32.const 96) (local.get $args_len)))
8241                    (drop (call $storage_write (i32.const 16) (i32.const 9) (i32.const 96) (local.get $args_len))))
8242                (func (export "record_call")
8243                    (call $record_call_impl))
8244                (func (export "add_bridge_validator")
8245                    (call $record_call_impl))
8246                (func (export "set_required_confirmations")
8247                    (call $record_call_impl))
8248                (func (export "set_request_timeout")
8249                    (call $record_call_impl))
8250                (func (export "add_price_feeder")
8251                    (call $record_call_impl))
8252                (func (export "set_authorized_attester")
8253                    (call $record_call_impl))
8254                (func (export "mb_pause")
8255                    (call $record_call_impl))
8256                (func (export "mb_unpause")
8257                    (call $record_call_impl))
8258                (func (export "cv_pause")
8259                    (call $record_call_impl))
8260                (func (export "cv_unpause")
8261                    (call $record_call_impl))
8262                (func (export "ms_pause")
8263                    (call $record_call_impl))
8264                (func (export "ms_unpause")
8265                    (call $record_call_impl))
8266                (func (export "pause")
8267                    (call $record_call_impl))
8268                (func (export "unpause")
8269                    (call $record_call_impl))
8270                (func (export "bb_pause")
8271                    (call $record_call_impl))
8272                (func (export "bb_unpause")
8273                    (call $record_call_impl))
8274                (func (export "emergency_pause")
8275                    (call $record_call_impl))
8276                (func (export "emergency_unpause")
8277                    (call $record_call_impl))
8278                (func (export "pause_pair")
8279                    (call $record_call_impl))
8280                (func (export "call")
8281                    (call $record_call_impl))
8282            )"#,
8283        )
8284        .expect("governance test contract should compile")
8285    }
8286
8287    fn wat_bytes(bytes: &[u8]) -> String {
8288        bytes.iter().map(|byte| format!("\\{:02x}", byte)).collect()
8289    }
8290
8291    fn reputation_reader_contract_code(rep_key: &[u8]) -> Vec<u8> {
8292        wat::parse_str(format!(
8293            r#"(module
8294                (import "env" "storage_read" (func $storage_read (param i32 i32 i32 i32) (result i32)))
8295                (import "env" "set_return_data" (func $set_return_data (param i32 i32) (result i32)))
8296                (memory (export "memory") 1)
8297                (data (i32.const 0) "{rep_key_data}")
8298                (func (export "read_reputation") (result i32)
8299                    (local $written i32)
8300                    (local.set $written
8301                        (call $storage_read (i32.const 0) (i32.const {rep_key_len}) (i32.const 96) (i32.const 8)))
8302                    (drop (call $set_return_data (i32.const 96) (local.get $written)))
8303                    (i32.const 0))
8304            )"#,
8305            rep_key_data = wat_bytes(rep_key),
8306            rep_key_len = rep_key.len(),
8307        ))
8308        .expect("reputation reader contract should compile")
8309    }
8310
8311    fn assert_governed_committee_contract_call_requires_proposal(
8312        function: &str,
8313        call_args: Vec<u8>,
8314    ) {
8315        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
8316        let validator = Pubkey([42u8; 32]);
8317        let bob_kp = Keypair::generate();
8318        let bob = bob_kp.pubkey();
8319        let gov_kp = Keypair::generate();
8320        let gov = gov_kp.pubkey();
8321
8322        let fund = Account::licn_to_spores(1_000);
8323        state
8324            .put_account(&alice, &Account::new(fund, alice))
8325            .unwrap();
8326        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
8327        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
8328        state.set_last_slot(0).unwrap();
8329        state.set_governance_authority(&gov).unwrap();
8330        state
8331            .set_governed_wallet_config(
8332                &gov,
8333                &crate::multisig::GovernedWalletConfig::new(
8334                    2,
8335                    vec![alice, bob, gov],
8336                    "community_treasury",
8337                )
8338                .with_timelock(1),
8339            )
8340            .unwrap();
8341
8342        let contract_addr =
8343            install_test_contract_account(&state, alice, governance_test_contract_code());
8344
8345        let direct = submit_contract_ix(
8346            &processor,
8347            &gov_kp,
8348            vec![gov, contract_addr],
8349            crate::ContractInstruction::Call {
8350                function: function.to_string(),
8351                args: call_args.clone(),
8352                value: 0,
8353            },
8354            genesis_hash,
8355            &validator,
8356        );
8357        assert!(!direct.success);
8358        assert!(direct
8359            .error
8360            .as_deref()
8361            .unwrap_or("")
8362            .contains("proposal flow"));
8363
8364        let propose_ix = Instruction {
8365            program_id: SYSTEM_PROGRAM_ID,
8366            accounts: vec![alice, gov, contract_addr],
8367            data: make_governance_contract_call_data(function, &call_args, 0),
8368        };
8369        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
8370        let result = processor.process_transaction(&propose_tx, &validator);
8371        assert!(
8372            result.success,
8373            "Proposal should succeed: {:?}",
8374            result.error
8375        );
8376
8377        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
8378        assert_eq!(proposal.approval_authority, None);
8379
8380        let mut approve_data = vec![35u8];
8381        approve_data.extend_from_slice(&1u64.to_le_bytes());
8382        let approve_ix = Instruction {
8383            program_id: SYSTEM_PROGRAM_ID,
8384            accounts: vec![bob],
8385            data: approve_data,
8386        };
8387        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
8388        let result = processor.process_transaction(&approve_tx, &validator);
8389        assert!(
8390            result.success,
8391            "Approval should succeed: {:?}",
8392            result.error
8393        );
8394
8395        let mut execute_data = vec![36u8];
8396        execute_data.extend_from_slice(&1u64.to_le_bytes());
8397        let execute_ix = Instruction {
8398            program_id: SYSTEM_PROGRAM_ID,
8399            accounts: vec![alice],
8400            data: execute_data.clone(),
8401        };
8402        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
8403        let result = processor.process_transaction(&execute_tx, &validator);
8404        assert!(!result.success);
8405        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
8406
8407        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
8408
8409        let execute_ix = Instruction {
8410            program_id: SYSTEM_PROGRAM_ID,
8411            accounts: vec![bob],
8412            data: execute_data,
8413        };
8414        let execute_tx = make_signed_tx(&bob_kp, execute_ix, fresh_blockhash);
8415        let result = processor.process_transaction(&execute_tx, &validator);
8416        assert!(
8417            result.success,
8418            "Execution should succeed: {:?}",
8419            result.error
8420        );
8421
8422        assert_eq!(
8423            state
8424                .get_contract_storage(&contract_addr, b"last_caller")
8425                .unwrap()
8426                .unwrap(),
8427            gov.0.to_vec()
8428        );
8429        assert_eq!(
8430            state
8431                .get_contract_storage(&contract_addr, b"last_args")
8432                .unwrap()
8433                .unwrap(),
8434            call_args
8435        );
8436    }
8437
8438    fn make_governance_contract_call_data(function: &str, args: &[u8], value: u64) -> Vec<u8> {
8439        let function_bytes = function.as_bytes();
8440        assert!(u16::try_from(function_bytes.len()).is_ok());
8441        let mut data = vec![34u8, GOVERNANCE_ACTION_CONTRACT_CALL];
8442        data.extend_from_slice(&value.to_le_bytes());
8443        data.extend_from_slice(&(function_bytes.len() as u16).to_le_bytes());
8444        data.extend_from_slice(function_bytes);
8445        data.extend_from_slice(&(args.len() as u32).to_le_bytes());
8446        data.extend_from_slice(args);
8447        data
8448    }
8449
8450    fn assert_contract_record_call_not_mutated(state: &StateStore, contract_addr: Pubkey) {
8451        assert_eq!(
8452            state
8453                .get_contract_storage(&contract_addr, b"last_args")
8454                .unwrap(),
8455            None
8456        );
8457        assert_eq!(
8458            state
8459                .get_contract_storage(&contract_addr, b"last_caller")
8460                .unwrap(),
8461            None
8462        );
8463    }
8464
8465    #[test]
8466    fn test_contract_lifecycle_active_allows_state_changing_wasm_execution() {
8467        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
8468        let validator = Pubkey([42u8; 32]);
8469        let contract_addr =
8470            install_test_contract_account(&state, alice, governance_test_contract_code());
8471        let args = vec![1, 2, 3];
8472
8473        let result = submit_contract_ix(
8474            &processor,
8475            &alice_kp,
8476            vec![alice, contract_addr],
8477            crate::ContractInstruction::Call {
8478                function: "record_call".to_string(),
8479                args: args.clone(),
8480                value: 0,
8481            },
8482            genesis_hash,
8483            &validator,
8484        );
8485
8486        assert!(
8487            result.success,
8488            "active call should succeed: {:?}",
8489            result.error
8490        );
8491        assert_eq!(
8492            state
8493                .get_contract_storage(&contract_addr, b"last_args")
8494                .unwrap()
8495                .unwrap(),
8496            args
8497        );
8498    }
8499
8500    #[test]
8501    fn test_contract_lifecycle_suspended_rejects_state_changing_wasm_before_execution() {
8502        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
8503        let validator = Pubkey([42u8; 32]);
8504        let contract_addr =
8505            install_test_contract_account(&state, alice, governance_test_contract_code());
8506        set_contract_lifecycle_status_for_test(
8507            &state,
8508            contract_addr,
8509            crate::ContractLifecycleStatus::Suspended,
8510        );
8511
8512        let before_caller_balance = state.get_balance(&alice).unwrap();
8513        let before_contract_balance = state.get_balance(&contract_addr).unwrap_or(0);
8514        let result = submit_contract_ix(
8515            &processor,
8516            &alice_kp,
8517            vec![alice, contract_addr],
8518            crate::ContractInstruction::Call {
8519                function: "record_call".to_string(),
8520                args: vec![4, 5, 6],
8521                value: 123,
8522            },
8523            genesis_hash,
8524            &validator,
8525        );
8526
8527        assert!(!result.success);
8528        assert!(result
8529            .error
8530            .as_deref()
8531            .unwrap_or("")
8532            .contains("lifecycle suspended"));
8533        assert_contract_record_call_not_mutated(&state, contract_addr);
8534        assert_eq!(
8535            state.get_balance(&contract_addr).unwrap_or(0),
8536            before_contract_balance
8537        );
8538        assert_eq!(
8539            before_caller_balance - state.get_balance(&alice).unwrap(),
8540            result.fee_paid
8541        );
8542    }
8543
8544    #[test]
8545    fn test_contract_lifecycle_quarantined_rejects_wasm_before_execution() {
8546        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
8547        let validator = Pubkey([42u8; 32]);
8548        let contract_addr =
8549            install_test_contract_account(&state, alice, governance_test_contract_code());
8550        set_contract_lifecycle_status_for_test(
8551            &state,
8552            contract_addr,
8553            crate::ContractLifecycleStatus::Quarantined,
8554        );
8555
8556        let result = submit_contract_ix(
8557            &processor,
8558            &alice_kp,
8559            vec![alice, contract_addr],
8560            crate::ContractInstruction::Call {
8561                function: "record_call".to_string(),
8562                args: vec![7],
8563                value: 0,
8564            },
8565            genesis_hash,
8566            &validator,
8567        );
8568
8569        assert!(!result.success);
8570        assert!(result
8571            .error
8572            .as_deref()
8573            .unwrap_or("")
8574            .contains("lifecycle quarantined"));
8575        assert_contract_record_call_not_mutated(&state, contract_addr);
8576    }
8577
8578    #[test]
8579    fn test_contract_lifecycle_terminated_rejects_wasm_before_execution() {
8580        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
8581        let validator = Pubkey([42u8; 32]);
8582        let contract_addr =
8583            install_test_contract_account(&state, alice, governance_test_contract_code());
8584        set_contract_lifecycle_status_for_test(
8585            &state,
8586            contract_addr,
8587            crate::ContractLifecycleStatus::Terminated,
8588        );
8589
8590        let result = submit_contract_ix(
8591            &processor,
8592            &alice_kp,
8593            vec![alice, contract_addr],
8594            crate::ContractInstruction::Call {
8595                function: "record_call".to_string(),
8596                args: vec![8],
8597                value: 0,
8598            },
8599            genesis_hash,
8600            &validator,
8601        );
8602
8603        assert!(!result.success);
8604        assert!(result
8605            .error
8606            .as_deref()
8607            .unwrap_or("")
8608            .contains("lifecycle terminated"));
8609        assert_contract_record_call_not_mutated(&state, contract_addr);
8610    }
8611
8612    #[test]
8613    fn test_contract_lifecycle_simulation_rejects_blocked_contract_before_execution() {
8614        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
8615        let contract_addr =
8616            install_test_contract_account(&state, alice, governance_test_contract_code());
8617        set_contract_lifecycle_status_for_test(
8618            &state,
8619            contract_addr,
8620            crate::ContractLifecycleStatus::Suspended,
8621        );
8622
8623        let ix = Instruction {
8624            program_id: CONTRACT_PROGRAM_ID,
8625            accounts: vec![alice, contract_addr],
8626            data: crate::ContractInstruction::Call {
8627                function: "record_call".to_string(),
8628                args: vec![9],
8629                value: 0,
8630            }
8631            .serialize()
8632            .unwrap(),
8633        };
8634        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
8635        let result = processor.simulate_transaction(&tx);
8636
8637        assert!(!result.success);
8638        assert!(result
8639            .error
8640            .as_deref()
8641            .unwrap_or("")
8642            .contains("lifecycle suspended"));
8643        assert_eq!(result.state_changes, 0);
8644        assert_contract_record_call_not_mutated(&state, contract_addr);
8645    }
8646
8647    #[test]
8648    fn restriction_governance_contract_suspend_and_lift_drive_lifecycle_without_owner_spoofing() {
8649        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
8650            setup_restriction_governance(2, 0);
8651        let validator = Pubkey([42u8; 32]);
8652        let contract_addr =
8653            install_test_contract_account(&state, alice, governance_test_contract_code());
8654
8655        let restrict_data = make_restrict_action_data(
8656            target_pubkey_payload(3, contract_addr),
8657            &RestrictionMode::StateChangingBlocked,
8658            RestrictionReason::TestnetDrill,
8659            None,
8660            None,
8661            None,
8662        );
8663        let result = process_governance_proposal(
8664            &processor,
8665            &alice_kp,
8666            alice,
8667            gov,
8668            restrict_data,
8669            genesis_hash,
8670            &validator,
8671        );
8672        assert!(
8673            result.success,
8674            "contract restriction proposal should be stored: {:?}",
8675            result.error
8676        );
8677        let result =
8678            process_governance_control(&processor, &bob_kp, bob, 35, 1, genesis_hash, &validator);
8679        assert!(
8680            result.success,
8681            "contract restriction approval should execute: {:?}",
8682            result.error
8683        );
8684
8685        let contract = load_contract_account_for_test(&state, contract_addr);
8686        assert_eq!(
8687            contract.lifecycle_status,
8688            crate::ContractLifecycleStatus::Suspended
8689        );
8690        assert_eq!(contract.lifecycle_restriction_id, Some(1));
8691        assert_eq!(contract.owner, alice);
8692
8693        let destination = Pubkey([0xE1; 32]);
8694        let close_result = submit_contract_ix(
8695            &processor,
8696            &bob_kp,
8697            vec![bob, contract_addr, destination],
8698            crate::ContractInstruction::Close,
8699            genesis_hash,
8700            &validator,
8701        );
8702        assert!(!close_result.success);
8703        assert!(close_result
8704            .error
8705            .as_deref()
8706            .unwrap_or("")
8707            .contains("Only contract owner can close"));
8708
8709        let blocked_call = submit_contract_ix(
8710            &processor,
8711            &alice_kp,
8712            vec![alice, contract_addr],
8713            crate::ContractInstruction::Call {
8714                function: "record_call".to_string(),
8715                args: vec![4, 5, 6],
8716                value: 0,
8717            },
8718            genesis_hash,
8719            &validator,
8720        );
8721        assert!(!blocked_call.success);
8722        assert!(blocked_call
8723            .error
8724            .as_deref()
8725            .unwrap_or("")
8726            .contains("lifecycle suspended"));
8727        assert_contract_record_call_not_mutated(&state, contract_addr);
8728
8729        let lift_data =
8730            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
8731        let result = process_governance_proposal(
8732            &processor,
8733            &alice_kp,
8734            alice,
8735            gov,
8736            lift_data,
8737            genesis_hash,
8738            &validator,
8739        );
8740        assert!(
8741            result.success,
8742            "contract restriction lift proposal should be stored: {:?}",
8743            result.error
8744        );
8745        let result =
8746            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
8747        assert!(
8748            result.success,
8749            "contract restriction lift should execute: {:?}",
8750            result.error
8751        );
8752
8753        let contract = load_contract_account_for_test(&state, contract_addr);
8754        assert_eq!(
8755            contract.lifecycle_status,
8756            crate::ContractLifecycleStatus::Active
8757        );
8758        assert_eq!(contract.lifecycle_restriction_id, None);
8759
8760        let args = vec![1, 2, 3];
8761        let allowed_call = submit_contract_ix(
8762            &processor,
8763            &alice_kp,
8764            vec![alice, contract_addr],
8765            crate::ContractInstruction::Call {
8766                function: "record_call".to_string(),
8767                args: args.clone(),
8768                value: 0,
8769            },
8770            genesis_hash,
8771            &validator,
8772        );
8773        assert!(
8774            allowed_call.success,
8775            "lifted contract call should succeed: {:?}",
8776            allowed_call.error
8777        );
8778        assert_eq!(
8779            state
8780                .get_contract_storage(&contract_addr, b"last_args")
8781                .unwrap()
8782                .unwrap(),
8783            args
8784        );
8785    }
8786
8787    #[test]
8788    fn restriction_governance_contract_temporary_restriction_expires_and_resumes_on_next_call() {
8789        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
8790            setup_restriction_governance(2, 5);
8791        let validator = Pubkey([42u8; 32]);
8792        let guardian_authority =
8793            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
8794        let contract_addr =
8795            install_test_contract_account(&state, alice, governance_test_contract_code());
8796
8797        create_active_guardian_test_restriction(
8798            &processor,
8799            &state,
8800            &alice_kp,
8801            alice,
8802            &bob_kp,
8803            bob,
8804            guardian_authority,
8805            genesis_hash,
8806            &validator,
8807            1,
8808            1,
8809            target_pubkey_payload(3, contract_addr),
8810            RestrictionMode::StateChangingBlocked,
8811            5,
8812        );
8813        let contract = load_contract_account_for_test(&state, contract_addr);
8814        assert_eq!(
8815            contract.lifecycle_status,
8816            crate::ContractLifecycleStatus::Suspended
8817        );
8818        assert_eq!(contract.lifecycle_restriction_id, Some(1));
8819
8820        let blocked_call = submit_contract_ix(
8821            &processor,
8822            &alice_kp,
8823            vec![alice, contract_addr],
8824            crate::ContractInstruction::Call {
8825                function: "record_call".to_string(),
8826                args: vec![9],
8827                value: 0,
8828            },
8829            genesis_hash,
8830            &validator,
8831        );
8832        assert!(!blocked_call.success);
8833        assert!(blocked_call
8834            .error
8835            .as_deref()
8836            .unwrap_or("")
8837            .contains("lifecycle suspended"));
8838
8839        let fresh_blockhash = advance_test_slot(&state, 5);
8840        let args = vec![10, 11];
8841        let resumed_call = submit_contract_ix(
8842            &processor,
8843            &alice_kp,
8844            vec![alice, contract_addr],
8845            crate::ContractInstruction::Call {
8846                function: "record_call".to_string(),
8847                args: args.clone(),
8848                value: 0,
8849            },
8850            fresh_blockhash,
8851            &validator,
8852        );
8853        assert!(
8854            resumed_call.success,
8855            "expired restriction should resume on next call: {:?}",
8856            resumed_call.error
8857        );
8858
8859        let contract = load_contract_account_for_test(&state, contract_addr);
8860        assert_eq!(
8861            contract.lifecycle_status,
8862            crate::ContractLifecycleStatus::Active
8863        );
8864        assert_eq!(contract.lifecycle_restriction_id, None);
8865        assert_eq!(
8866            state
8867                .get_contract_storage(&contract_addr, b"last_args")
8868                .unwrap()
8869                .unwrap(),
8870            args
8871        );
8872    }
8873
8874    #[test]
8875    fn restriction_governance_contract_termination_is_permanent_and_preserves_state() {
8876        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
8877            setup_restriction_governance(2, 1);
8878        let validator = Pubkey([42u8; 32]);
8879        let contract_addr =
8880            install_test_contract_account(&state, alice, governance_test_contract_code());
8881        let preserved_balance = Account::licn_to_spores(25);
8882        let preserved_storage = b"audit_value".to_vec();
8883        let mut account = state.get_account(&contract_addr).unwrap().unwrap();
8884        account.spores = preserved_balance;
8885        account.spendable = preserved_balance;
8886        state.put_account(&contract_addr, &account).unwrap();
8887        state
8888            .put_contract_storage(&contract_addr, b"audit_key", &preserved_storage)
8889            .unwrap();
8890
8891        let terminate_data = make_restrict_action_data(
8892            target_pubkey_payload(3, contract_addr),
8893            &RestrictionMode::Terminated,
8894            RestrictionReason::TestnetDrill,
8895            None,
8896            None,
8897            None,
8898        );
8899        let result = process_governance_proposal(
8900            &processor,
8901            &alice_kp,
8902            alice,
8903            gov,
8904            terminate_data,
8905            genesis_hash,
8906            &validator,
8907        );
8908        assert!(
8909            result.success,
8910            "termination proposal should be stored: {:?}",
8911            result.error
8912        );
8913        let result =
8914            process_governance_control(&processor, &bob_kp, bob, 35, 1, genesis_hash, &validator);
8915        assert!(
8916            result.success,
8917            "termination approval should be recorded: {:?}",
8918            result.error
8919        );
8920        let early_execute =
8921            process_governance_control(&processor, &bob_kp, bob, 36, 1, genesis_hash, &validator);
8922        assert!(!early_execute.success);
8923        assert!(early_execute
8924            .error
8925            .as_deref()
8926            .unwrap_or("")
8927            .contains("timelocked"));
8928
8929        let terminate_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
8930        let result = process_governance_control(
8931            &processor,
8932            &bob_kp,
8933            bob,
8934            36,
8935            1,
8936            terminate_blockhash,
8937            &validator,
8938        );
8939        assert!(
8940            result.success,
8941            "termination should execute after timelock: {:?}",
8942            result.error
8943        );
8944
8945        let account = state.get_account(&contract_addr).unwrap().unwrap();
8946        assert!(account.executable);
8947        assert!(!account.data.is_empty());
8948        assert_eq!(account.spores, preserved_balance);
8949        assert_eq!(account.spendable, preserved_balance);
8950        assert_eq!(
8951            state
8952                .get_contract_storage(&contract_addr, b"audit_key")
8953                .unwrap()
8954                .unwrap(),
8955            preserved_storage
8956        );
8957        let contract = load_contract_account_for_test(&state, contract_addr);
8958        assert_eq!(
8959            contract.lifecycle_status,
8960            crate::ContractLifecycleStatus::Terminated
8961        );
8962        assert_eq!(contract.lifecycle_restriction_id, Some(1));
8963        assert_eq!(contract.owner, alice);
8964
8965        let blocked_call = submit_contract_ix(
8966            &processor,
8967            &alice_kp,
8968            vec![alice, contract_addr],
8969            crate::ContractInstruction::Call {
8970                function: "record_call".to_string(),
8971                args: vec![12],
8972                value: 0,
8973            },
8974            terminate_blockhash,
8975            &validator,
8976        );
8977        assert!(!blocked_call.success);
8978        assert!(blocked_call
8979            .error
8980            .as_deref()
8981            .unwrap_or("")
8982            .contains("lifecycle terminated"));
8983        assert_contract_record_call_not_mutated(&state, contract_addr);
8984
8985        let lift_data =
8986            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
8987        let result = process_governance_proposal(
8988            &processor,
8989            &alice_kp,
8990            alice,
8991            gov,
8992            lift_data,
8993            terminate_blockhash,
8994            &validator,
8995        );
8996        assert!(
8997            result.success,
8998            "terminated lift proposal should be stored until execution gate: {:?}",
8999            result.error
9000        );
9001        let result = process_governance_control(
9002            &processor,
9003            &bob_kp,
9004            bob,
9005            35,
9006            2,
9007            terminate_blockhash,
9008            &validator,
9009        );
9010        assert!(
9011            result.success,
9012            "terminated lift approval should be recorded: {:?}",
9013            result.error
9014        );
9015        let lift_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH * 2);
9016        let result =
9017            process_governance_control(&processor, &bob_kp, bob, 36, 2, lift_blockhash, &validator);
9018        assert!(!result.success);
9019        assert!(result
9020            .error
9021            .as_deref()
9022            .unwrap_or("")
9023            .contains("cannot be lifted"));
9024
9025        let extend_data = make_extend_restriction_action_data(1, Some(SLOTS_PER_EPOCH * 4), None);
9026        let result = process_governance_proposal(
9027            &processor,
9028            &alice_kp,
9029            alice,
9030            gov,
9031            extend_data,
9032            lift_blockhash,
9033            &validator,
9034        );
9035        assert!(
9036            result.success,
9037            "terminated extension proposal should be stored until execution gate: {:?}",
9038            result.error
9039        );
9040        let result =
9041            process_governance_control(&processor, &bob_kp, bob, 35, 3, lift_blockhash, &validator);
9042        assert!(
9043            result.success,
9044            "terminated extension approval should be recorded: {:?}",
9045            result.error
9046        );
9047        let extend_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH * 3);
9048        let result = process_governance_control(
9049            &processor,
9050            &bob_kp,
9051            bob,
9052            36,
9053            3,
9054            extend_blockhash,
9055            &validator,
9056        );
9057        assert!(!result.success);
9058        assert!(result
9059            .error
9060            .as_deref()
9061            .unwrap_or("")
9062            .contains("cannot be extended"));
9063
9064        let account = state.get_account(&contract_addr).unwrap().unwrap();
9065        assert!(account.executable);
9066        assert_eq!(account.spendable, preserved_balance);
9067        let contract = load_contract_account_for_test(&state, contract_addr);
9068        assert_eq!(
9069            contract.lifecycle_status,
9070            crate::ContractLifecycleStatus::Terminated
9071        );
9072        assert_eq!(contract.lifecycle_restriction_id, Some(1));
9073    }
9074
9075    fn setup_restriction_governance(
9076        threshold: u8,
9077        timelock_epochs: u32,
9078    ) -> (
9079        TxProcessor,
9080        StateStore,
9081        Keypair,
9082        Pubkey,
9083        Keypair,
9084        Pubkey,
9085        Pubkey,
9086        Hash,
9087    ) {
9088        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
9089        let bob_kp = Keypair::generate();
9090        let bob = bob_kp.pubkey();
9091        let gov = Pubkey([0xA7; 32]);
9092        let fund = Account::licn_to_spores(1_000);
9093
9094        state
9095            .put_account(&alice, &Account::new(fund, alice))
9096            .unwrap();
9097        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
9098        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
9099        state.set_last_slot(0).unwrap();
9100        state.set_governance_authority(&gov).unwrap();
9101        state
9102            .set_governed_wallet_config(
9103                &gov,
9104                &crate::multisig::GovernedWalletConfig::new(
9105                    threshold,
9106                    vec![alice, bob],
9107                    "community_treasury",
9108                )
9109                .with_timelock(timelock_epochs),
9110            )
9111            .unwrap();
9112
9113        (
9114            processor,
9115            state,
9116            alice_kp,
9117            alice,
9118            bob_kp,
9119            bob,
9120            gov,
9121            genesis_hash,
9122        )
9123    }
9124
9125    fn make_governance_proposal_ix(
9126        proposer: Pubkey,
9127        authority: Pubkey,
9128        data: Vec<u8>,
9129    ) -> Instruction {
9130        Instruction {
9131            program_id: SYSTEM_PROGRAM_ID,
9132            accounts: vec![proposer, authority],
9133            data,
9134        }
9135    }
9136
9137    fn make_governance_proposal_control_data(instruction_type: u8, proposal_id: u64) -> Vec<u8> {
9138        let mut data = vec![instruction_type];
9139        data.extend_from_slice(&proposal_id.to_le_bytes());
9140        data
9141    }
9142
9143    fn process_governance_proposal(
9144        processor: &TxProcessor,
9145        signer: &Keypair,
9146        signer_pubkey: Pubkey,
9147        authority: Pubkey,
9148        data: Vec<u8>,
9149        recent_blockhash: Hash,
9150        validator: &Pubkey,
9151    ) -> TxResult {
9152        let ix = make_governance_proposal_ix(signer_pubkey, authority, data);
9153        let tx = make_signed_tx(signer, ix, recent_blockhash);
9154        processor.process_transaction(&tx, validator)
9155    }
9156
9157    fn process_governance_control(
9158        processor: &TxProcessor,
9159        signer: &Keypair,
9160        signer_pubkey: Pubkey,
9161        instruction_type: u8,
9162        proposal_id: u64,
9163        recent_blockhash: Hash,
9164        validator: &Pubkey,
9165    ) -> TxResult {
9166        let ix = Instruction {
9167            program_id: SYSTEM_PROGRAM_ID,
9168            accounts: vec![signer_pubkey],
9169            data: make_governance_proposal_control_data(instruction_type, proposal_id),
9170        };
9171        let tx = make_signed_tx(signer, ix, recent_blockhash);
9172        processor.process_transaction(&tx, validator)
9173    }
9174
9175    fn parse_governance_action_with_accounts(
9176        data: Vec<u8>,
9177        accounts: Vec<Pubkey>,
9178    ) -> Result<GovernanceAction, String> {
9179        let (processor, _state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
9180        let ix = Instruction {
9181            program_id: SYSTEM_PROGRAM_ID,
9182            accounts,
9183            data,
9184        };
9185        processor
9186            .parse_governance_action(&ix)
9187            .map(|(_proposer, _authority, action)| action)
9188    }
9189
9190    fn parse_restriction_governance_action(data: Vec<u8>) -> Result<GovernanceAction, String> {
9191        parse_governance_action_with_accounts(data, vec![Pubkey([0xA0; 32]), Pubkey([0xA1; 32])])
9192    }
9193
9194    fn target_account_payload(account: Pubkey) -> Vec<u8> {
9195        let mut payload = vec![0u8];
9196        payload.extend_from_slice(&account.0);
9197        payload
9198    }
9199
9200    fn target_account_asset_payload(account: Pubkey, asset: Pubkey) -> Vec<u8> {
9201        let mut payload = vec![1u8];
9202        payload.extend_from_slice(&account.0);
9203        payload.extend_from_slice(&asset.0);
9204        payload
9205    }
9206
9207    fn target_pubkey_payload(target_type: u8, pubkey: Pubkey) -> Vec<u8> {
9208        let mut payload = vec![target_type];
9209        payload.extend_from_slice(&pubkey.0);
9210        payload
9211    }
9212
9213    fn target_code_hash_payload(code_hash: Hash) -> Vec<u8> {
9214        let mut payload = vec![4u8];
9215        payload.extend_from_slice(&code_hash.0);
9216        payload
9217    }
9218
9219    fn push_limited_string(payload: &mut Vec<u8>, value: &str) {
9220        let value_bytes = value.as_bytes();
9221        assert!(u16::try_from(value_bytes.len()).is_ok());
9222        payload.extend_from_slice(&(value_bytes.len() as u16).to_le_bytes());
9223        payload.extend_from_slice(value_bytes);
9224    }
9225
9226    fn target_bridge_route_payload(chain_id: &str, asset: &str) -> Vec<u8> {
9227        let mut payload = vec![5u8];
9228        push_limited_string(&mut payload, chain_id);
9229        push_limited_string(&mut payload, asset);
9230        payload
9231    }
9232
9233    fn target_protocol_module_payload(module: ProtocolModuleId) -> Vec<u8> {
9234        vec![6u8, module.as_u8()]
9235    }
9236
9237    fn frozen_mode_amount(mode: &RestrictionMode) -> Option<u64> {
9238        match mode {
9239            RestrictionMode::FrozenAmount { amount } => Some(*amount),
9240            _ => None,
9241        }
9242    }
9243
9244    fn make_restrict_action_data(
9245        target_payload: Vec<u8>,
9246        mode: &RestrictionMode,
9247        reason: RestrictionReason,
9248        evidence_hash: Option<Hash>,
9249        evidence_uri_hash: Option<Hash>,
9250        expires_at_slot: Option<u64>,
9251    ) -> Vec<u8> {
9252        let mut data = vec![34u8, GOVERNANCE_ACTION_RESTRICT];
9253        data.extend(target_payload);
9254        data.push(mode.mode_id());
9255        if let Some(amount) = frozen_mode_amount(mode) {
9256            data.extend_from_slice(&amount.to_le_bytes());
9257        }
9258        data.push(reason.as_u8());
9259        let mut flags = 0u8;
9260        if evidence_hash.is_some() {
9261            flags |= 0x01;
9262        }
9263        if evidence_uri_hash.is_some() {
9264            flags |= 0x02;
9265        }
9266        if expires_at_slot.is_some() {
9267            flags |= 0x04;
9268        }
9269        data.push(flags);
9270        if let Some(hash) = evidence_hash {
9271            data.extend_from_slice(&hash.0);
9272        }
9273        if let Some(hash) = evidence_uri_hash {
9274            data.extend_from_slice(&hash.0);
9275        }
9276        if let Some(slot) = expires_at_slot {
9277            data.extend_from_slice(&slot.to_le_bytes());
9278        }
9279        data
9280    }
9281
9282    fn make_lift_restriction_action_data(
9283        restriction_id: u64,
9284        reason: RestrictionLiftReason,
9285    ) -> Vec<u8> {
9286        let mut data = vec![34u8, GOVERNANCE_ACTION_LIFT_RESTRICTION];
9287        data.extend_from_slice(&restriction_id.to_le_bytes());
9288        data.push(reason.as_u8());
9289        data
9290    }
9291
9292    fn make_extend_restriction_action_data(
9293        restriction_id: u64,
9294        new_expires_at_slot: Option<u64>,
9295        evidence_hash: Option<Hash>,
9296    ) -> Vec<u8> {
9297        let mut data = vec![34u8, GOVERNANCE_ACTION_EXTEND_RESTRICTION];
9298        data.extend_from_slice(&restriction_id.to_le_bytes());
9299        let mut flags = 0u8;
9300        if new_expires_at_slot.is_some() {
9301            flags |= 0x01;
9302        }
9303        if evidence_hash.is_some() {
9304            flags |= 0x02;
9305        }
9306        data.push(flags);
9307        if let Some(slot) = new_expires_at_slot {
9308            data.extend_from_slice(&slot.to_le_bytes());
9309        }
9310        if let Some(hash) = evidence_hash {
9311            data.extend_from_slice(&hash.0);
9312        }
9313        data
9314    }
9315
9316    #[allow(clippy::too_many_arguments)]
9317    fn create_active_test_restriction(
9318        processor: &TxProcessor,
9319        state: &StateStore,
9320        alice_kp: &Keypair,
9321        alice: Pubkey,
9322        bob_kp: &Keypair,
9323        bob: Pubkey,
9324        gov: Pubkey,
9325        genesis_hash: Hash,
9326        validator: &Pubkey,
9327        expires_at_slot: Option<u64>,
9328    ) {
9329        let target = Pubkey([0xC1; 32]);
9330        let data = make_restrict_action_data(
9331            target_account_payload(target),
9332            &RestrictionMode::OutgoingOnly,
9333            RestrictionReason::TestnetDrill,
9334            None,
9335            None,
9336            expires_at_slot,
9337        );
9338        let result = process_governance_proposal(
9339            processor,
9340            alice_kp,
9341            alice,
9342            gov,
9343            data,
9344            genesis_hash,
9345            validator,
9346        );
9347        assert!(
9348            result.success,
9349            "Restriction proposal should succeed: {:?}",
9350            result.error
9351        );
9352        assert!(state.get_restriction(1).unwrap().is_none());
9353
9354        let result =
9355            process_governance_control(processor, bob_kp, bob, 35, 1, genesis_hash, validator);
9356        assert!(
9357            result.success,
9358            "Restriction approval should execute: {:?}",
9359            result.error
9360        );
9361
9362        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
9363        assert!(proposal.executed);
9364        assert_eq!(
9365            state.get_restriction(1).unwrap().unwrap().status,
9366            RestrictionStatus::Active
9367        );
9368    }
9369
9370    #[allow(clippy::too_many_arguments)]
9371    fn create_active_bridge_split_restriction(
9372        processor: &TxProcessor,
9373        state: &StateStore,
9374        alice_kp: &Keypair,
9375        alice: Pubkey,
9376        bob_kp: &Keypair,
9377        bob: Pubkey,
9378        bridge_authority: Pubkey,
9379        genesis_hash: Hash,
9380        validator: &Pubkey,
9381    ) -> Hash {
9382        let data = make_restrict_action_data(
9383            target_bridge_route_payload("neo-x-testnet", "USDT"),
9384            &RestrictionMode::RoutePaused,
9385            RestrictionReason::TestnetDrill,
9386            None,
9387            None,
9388            Some(SLOTS_PER_EPOCH * 4),
9389        );
9390        let result = process_governance_proposal(
9391            processor,
9392            alice_kp,
9393            alice,
9394            bridge_authority,
9395            data,
9396            genesis_hash,
9397            validator,
9398        );
9399        assert!(
9400            result.success,
9401            "Bridge split restriction proposal should succeed: {:?}",
9402            result.error
9403        );
9404
9405        let result =
9406            process_governance_control(processor, bob_kp, bob, 35, 1, genesis_hash, validator);
9407        assert!(
9408            result.success,
9409            "Bridge split restriction approval should succeed: {:?}",
9410            result.error
9411        );
9412        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
9413        assert!(!proposal.executed);
9414
9415        let fresh_blockhash = advance_test_slot(state, SLOTS_PER_EPOCH);
9416        let result =
9417            process_governance_control(processor, bob_kp, bob, 36, 1, fresh_blockhash, validator);
9418        assert!(
9419            result.success,
9420            "Bridge split restriction execution should succeed: {:?}",
9421            result.error
9422        );
9423
9424        let record = state.get_restriction(1).unwrap().unwrap();
9425        assert_eq!(record.status, RestrictionStatus::Active);
9426        assert_eq!(record.approval_authority, Some(bridge_authority));
9427        fresh_blockhash
9428    }
9429
9430    #[allow(clippy::too_many_arguments)]
9431    fn create_active_guardian_test_restriction(
9432        processor: &TxProcessor,
9433        state: &StateStore,
9434        alice_kp: &Keypair,
9435        alice: Pubkey,
9436        bob_kp: &Keypair,
9437        bob: Pubkey,
9438        guardian_authority: Pubkey,
9439        genesis_hash: Hash,
9440        validator: &Pubkey,
9441        proposal_id: u64,
9442        restriction_id: u64,
9443        target_payload: Vec<u8>,
9444        mode: RestrictionMode,
9445        expires_at_slot: u64,
9446    ) -> RestrictionRecord {
9447        let data = make_restrict_action_data(
9448            target_payload,
9449            &mode,
9450            RestrictionReason::TestnetDrill,
9451            None,
9452            None,
9453            Some(expires_at_slot),
9454        );
9455        let result = process_governance_proposal(
9456            processor,
9457            alice_kp,
9458            alice,
9459            guardian_authority,
9460            data,
9461            genesis_hash,
9462            validator,
9463        );
9464        assert!(
9465            result.success,
9466            "Guardian restriction proposal should succeed: {:?}",
9467            result.error
9468        );
9469        let proposal = state.get_governance_proposal(proposal_id).unwrap().unwrap();
9470        assert_eq!(
9471            proposal.authority,
9472            state.get_governance_authority().unwrap().unwrap()
9473        );
9474        assert_eq!(proposal.approval_authority, Some(guardian_authority));
9475        assert!(!proposal.executed);
9476
9477        let result = process_governance_control(
9478            processor,
9479            bob_kp,
9480            bob,
9481            35,
9482            proposal_id,
9483            genesis_hash,
9484            validator,
9485        );
9486        assert!(
9487            result.success,
9488            "Guardian restriction approval should execute: {:?}",
9489            result.error
9490        );
9491        let proposal = state.get_governance_proposal(proposal_id).unwrap().unwrap();
9492        assert!(proposal.executed);
9493
9494        let record = state.get_restriction(restriction_id).unwrap().unwrap();
9495        assert_eq!(record.id, restriction_id);
9496        assert_eq!(record.mode, mode);
9497        assert_eq!(record.status, RestrictionStatus::Active);
9498        assert_eq!(record.approval_authority, Some(guardian_authority));
9499        assert_eq!(record.expires_at_slot, Some(expires_at_slot));
9500        record
9501    }
9502
9503    fn find_system_event<'a>(
9504        events: &'a [ContractEvent],
9505        event_name: &str,
9506        proposal_id: u64,
9507    ) -> &'a ContractEvent {
9508        let proposal_id = proposal_id.to_string();
9509        events
9510            .iter()
9511            .find(|event| {
9512                event.program == SYSTEM_PROGRAM_ID
9513                    && event.name == event_name
9514                    && event.data.get("proposal_id").map(String::as_str)
9515                        == Some(proposal_id.as_str())
9516            })
9517            .unwrap_or_else(|| panic!("missing {} for proposal {}", event_name, proposal_id))
9518    }
9519
9520    fn assert_restrict_action(
9521        action: GovernanceAction,
9522        expected_target: RestrictionTarget,
9523        expected_mode: RestrictionMode,
9524        expected_reason: RestrictionReason,
9525        expected_evidence_hash: Option<Hash>,
9526        expected_evidence_uri_hash: Option<Hash>,
9527        expected_expires_at_slot: Option<u64>,
9528    ) {
9529        match action {
9530            GovernanceAction::Restrict {
9531                target,
9532                mode,
9533                reason,
9534                evidence_hash,
9535                evidence_uri_hash,
9536                expires_at_slot,
9537            } => {
9538                assert_eq!(target, expected_target);
9539                assert_eq!(mode, expected_mode);
9540                assert_eq!(reason, expected_reason);
9541                assert_eq!(evidence_hash, expected_evidence_hash);
9542                assert_eq!(evidence_uri_hash, expected_evidence_uri_hash);
9543                assert_eq!(expires_at_slot, expected_expires_at_slot);
9544            }
9545            other => panic!("expected Restrict action, got {:?}", other),
9546        }
9547    }
9548
9549    fn assert_parse_error(data: Vec<u8>, expected: &str) {
9550        let err = parse_restriction_governance_action(data).expect_err("parse should fail");
9551        assert!(
9552            err.contains(expected),
9553            "expected error containing {:?}, got {:?}",
9554            expected,
9555            err
9556        );
9557    }
9558
9559    #[test]
9560    fn test_restriction_governance_action_subtypes_are_append_only() {
9561        assert_eq!(GOVERNANCE_ACTION_TREASURY_TRANSFER, 0);
9562        assert_eq!(GOVERNANCE_ACTION_PARAM_CHANGE, 1);
9563        assert_eq!(GOVERNANCE_ACTION_CONTRACT_UPGRADE, 2);
9564        assert_eq!(GOVERNANCE_ACTION_SET_UPGRADE_TIMELOCK, 3);
9565        assert_eq!(GOVERNANCE_ACTION_EXECUTE_UPGRADE, 4);
9566        assert_eq!(GOVERNANCE_ACTION_VETO_UPGRADE, 5);
9567        assert_eq!(GOVERNANCE_ACTION_CONTRACT_CLOSE, 6);
9568        assert_eq!(GOVERNANCE_ACTION_REGISTER_SYMBOL, 7);
9569        assert_eq!(GOVERNANCE_ACTION_SET_CONTRACT_ABI, 8);
9570        assert_eq!(GOVERNANCE_ACTION_CONTRACT_CALL, 9);
9571        assert_eq!(GOVERNANCE_ACTION_RESTRICT, 10);
9572        assert_eq!(GOVERNANCE_ACTION_LIFT_RESTRICTION, 11);
9573        assert_eq!(GOVERNANCE_ACTION_EXTEND_RESTRICTION, 12);
9574
9575        let proposer = Pubkey([0x01; 32]);
9576        let authority = Pubkey([0x02; 32]);
9577        let recipient = Pubkey([0x03; 32]);
9578        let mut treasury_data = vec![34u8, GOVERNANCE_ACTION_TREASURY_TRANSFER];
9579        treasury_data.extend_from_slice(&500u64.to_le_bytes());
9580        match parse_governance_action_with_accounts(
9581            treasury_data,
9582            vec![proposer, authority, recipient],
9583        )
9584        .expect("legacy treasury subtype parses")
9585        {
9586            GovernanceAction::TreasuryTransfer {
9587                recipient: parsed_recipient,
9588                amount,
9589            } => {
9590                assert_eq!(parsed_recipient, recipient);
9591                assert_eq!(amount, 500);
9592            }
9593            other => panic!("expected TreasuryTransfer, got {:?}", other),
9594        }
9595
9596        let contract = Pubkey([0x04; 32]);
9597        match parse_governance_action_with_accounts(
9598            make_governance_contract_call_data("record_call", &[1, 2, 3], 7),
9599            vec![proposer, authority, contract],
9600        )
9601        .expect("legacy contract call subtype parses")
9602        {
9603            GovernanceAction::ContractCall {
9604                contract: parsed_contract,
9605                function,
9606                args,
9607                value,
9608            } => {
9609                assert_eq!(parsed_contract, contract);
9610                assert_eq!(function, "record_call");
9611                assert_eq!(args, vec![1, 2, 3]);
9612                assert_eq!(value, 7);
9613            }
9614            other => panic!("expected ContractCall, got {:?}", other),
9615        }
9616    }
9617
9618    #[test]
9619    fn test_parse_restrict_governance_action_target_forms() {
9620        let cases = vec![
9621            (
9622                target_account_payload(Pubkey([0x10; 32])),
9623                RestrictionTarget::Account(Pubkey([0x10; 32])),
9624                RestrictionMode::OutgoingOnly,
9625                RestrictionReason::TestnetDrill,
9626                None,
9627                None,
9628                Some(101),
9629            ),
9630            (
9631                target_account_asset_payload(Pubkey([0x11; 32]), Pubkey([0x12; 32])),
9632                RestrictionTarget::AccountAsset {
9633                    account: Pubkey([0x11; 32]),
9634                    asset: Pubkey([0x12; 32]),
9635                },
9636                RestrictionMode::FrozenAmount { amount: 55 },
9637                RestrictionReason::StolenFunds,
9638                Some(Hash([0x21; 32])),
9639                None,
9640                Some(102),
9641            ),
9642            (
9643                target_pubkey_payload(2, Pubkey([0x13; 32])),
9644                RestrictionTarget::Asset(Pubkey([0x13; 32])),
9645                RestrictionMode::AssetPaused,
9646                RestrictionReason::CustodyIncident,
9647                None,
9648                Some(Hash([0x22; 32])),
9649                Some(103),
9650            ),
9651            (
9652                target_pubkey_payload(3, Pubkey([0x14; 32])),
9653                RestrictionTarget::Contract(Pubkey([0x14; 32])),
9654                RestrictionMode::Quarantined,
9655                RestrictionReason::ScamContract,
9656                Some(Hash([0x23; 32])),
9657                None,
9658                Some(104),
9659            ),
9660            (
9661                target_code_hash_payload(Hash([0x15; 32])),
9662                RestrictionTarget::CodeHash(Hash([0x15; 32])),
9663                RestrictionMode::DeployBlocked,
9664                RestrictionReason::MaliciousCodeHash,
9665                Some(Hash([0x24; 32])),
9666                None,
9667                Some(105),
9668            ),
9669            (
9670                target_bridge_route_payload("neo-x-testnet", "WETH"),
9671                RestrictionTarget::BridgeRoute {
9672                    chain_id: "neo-x-testnet".to_string(),
9673                    asset: "WETH".to_string(),
9674                },
9675                RestrictionMode::RoutePaused,
9676                RestrictionReason::BridgeCompromise,
9677                Some(Hash([0x25; 32])),
9678                None,
9679                Some(106),
9680            ),
9681            (
9682                target_protocol_module_payload(ProtocolModuleId::Mempool),
9683                RestrictionTarget::ProtocolModule(ProtocolModuleId::Mempool),
9684                RestrictionMode::ProtocolPaused,
9685                RestrictionReason::ProtocolBug,
9686                Some(Hash([0x26; 32])),
9687                None,
9688                Some(107),
9689            ),
9690        ];
9691
9692        for (
9693            target_payload,
9694            target,
9695            mode,
9696            reason,
9697            evidence_hash,
9698            evidence_uri_hash,
9699            expires_at_slot,
9700        ) in cases
9701        {
9702            let data = make_restrict_action_data(
9703                target_payload,
9704                &mode,
9705                reason,
9706                evidence_hash,
9707                evidence_uri_hash,
9708                expires_at_slot,
9709            );
9710            let action = parse_restriction_governance_action(data).expect("restrict parses");
9711            assert_restrict_action(
9712                action,
9713                target,
9714                mode,
9715                reason,
9716                evidence_hash,
9717                evidence_uri_hash,
9718                expires_at_slot,
9719            );
9720        }
9721    }
9722
9723    #[test]
9724    fn test_parse_restrict_governance_action_modes() {
9725        let cases = vec![
9726            (
9727                target_account_payload(Pubkey([0x30; 32])),
9728                RestrictionMode::OutgoingOnly,
9729            ),
9730            (
9731                target_account_payload(Pubkey([0x31; 32])),
9732                RestrictionMode::IncomingOnly,
9733            ),
9734            (
9735                target_account_payload(Pubkey([0x32; 32])),
9736                RestrictionMode::Bidirectional,
9737            ),
9738            (
9739                target_account_asset_payload(Pubkey([0x33; 32]), Pubkey([0x34; 32])),
9740                RestrictionMode::FrozenAmount { amount: 77 },
9741            ),
9742            (
9743                target_pubkey_payload(2, Pubkey([0x35; 32])),
9744                RestrictionMode::AssetPaused,
9745            ),
9746            (
9747                target_pubkey_payload(3, Pubkey([0x36; 32])),
9748                RestrictionMode::ExecuteBlocked,
9749            ),
9750            (
9751                target_pubkey_payload(3, Pubkey([0x37; 32])),
9752                RestrictionMode::StateChangingBlocked,
9753            ),
9754            (
9755                target_pubkey_payload(3, Pubkey([0x38; 32])),
9756                RestrictionMode::Quarantined,
9757            ),
9758            (
9759                target_code_hash_payload(Hash([0x39; 32])),
9760                RestrictionMode::DeployBlocked,
9761            ),
9762            (
9763                target_bridge_route_payload("eth-mainnet", "USDT"),
9764                RestrictionMode::RoutePaused,
9765            ),
9766            (
9767                target_protocol_module_payload(ProtocolModuleId::Bridge),
9768                RestrictionMode::ProtocolPaused,
9769            ),
9770            (
9771                target_pubkey_payload(3, Pubkey([0x3A; 32])),
9772                RestrictionMode::Terminated,
9773            ),
9774        ];
9775
9776        for (target_payload, mode) in cases {
9777            let data = make_restrict_action_data(
9778                target_payload,
9779                &mode,
9780                RestrictionReason::TestnetDrill,
9781                None,
9782                None,
9783                None,
9784            );
9785            match parse_restriction_governance_action(data).expect("mode parses") {
9786                GovernanceAction::Restrict {
9787                    mode: parsed_mode, ..
9788                } => assert_eq!(parsed_mode, mode),
9789                other => panic!("expected Restrict action, got {:?}", other),
9790            }
9791        }
9792    }
9793
9794    #[test]
9795    fn test_parse_lift_and_extend_restriction_governance_actions() {
9796        match parse_restriction_governance_action(make_lift_restriction_action_data(
9797            42,
9798            RestrictionLiftReason::FalsePositive,
9799        ))
9800        .expect("lift parses")
9801        {
9802            GovernanceAction::LiftRestriction {
9803                restriction_id,
9804                reason,
9805            } => {
9806                assert_eq!(restriction_id, 42);
9807                assert_eq!(reason, RestrictionLiftReason::FalsePositive);
9808            }
9809            other => panic!("expected LiftRestriction, got {:?}", other),
9810        }
9811
9812        match parse_restriction_governance_action(make_extend_restriction_action_data(
9813            43,
9814            Some(1_000),
9815            Some(Hash([0x44; 32])),
9816        ))
9817        .expect("extend parses")
9818        {
9819            GovernanceAction::ExtendRestriction {
9820                restriction_id,
9821                new_expires_at_slot,
9822                evidence_hash,
9823            } => {
9824                assert_eq!(restriction_id, 43);
9825                assert_eq!(new_expires_at_slot, Some(1_000));
9826                assert_eq!(evidence_hash, Some(Hash([0x44; 32])));
9827            }
9828            other => panic!("expected ExtendRestriction, got {:?}", other),
9829        }
9830
9831        match parse_restriction_governance_action(make_extend_restriction_action_data(
9832            44, None, None,
9833        ))
9834        .expect("no-op shaped extend parses for execution-time validation")
9835        {
9836            GovernanceAction::ExtendRestriction {
9837                restriction_id,
9838                new_expires_at_slot,
9839                evidence_hash,
9840            } => {
9841                assert_eq!(restriction_id, 44);
9842                assert_eq!(new_expires_at_slot, None);
9843                assert_eq!(evidence_hash, None);
9844            }
9845            other => panic!("expected ExtendRestriction, got {:?}", other),
9846        }
9847    }
9848
9849    #[test]
9850    fn test_parse_restriction_governance_action_rejects_malformed_payloads() {
9851        assert_parse_error(vec![34u8, 13], "Unknown governance action type 13");
9852
9853        let mut unknown_target = vec![34u8, GOVERNANCE_ACTION_RESTRICT, 99];
9854        unknown_target.push(RestrictionMode::OutgoingOnly.mode_id());
9855        unknown_target.push(RestrictionReason::TestnetDrill.as_u8());
9856        unknown_target.push(0);
9857        assert_parse_error(unknown_target, "unknown restriction target type 99");
9858
9859        let mut unknown_mode = vec![34u8, GOVERNANCE_ACTION_RESTRICT];
9860        unknown_mode.extend(target_account_payload(Pubkey([0x50; 32])));
9861        unknown_mode.push(99);
9862        unknown_mode.push(RestrictionReason::TestnetDrill.as_u8());
9863        unknown_mode.push(0);
9864        assert_parse_error(unknown_mode, "unknown restriction mode 99");
9865
9866        let mut missing_frozen_amount = vec![34u8, GOVERNANCE_ACTION_RESTRICT];
9867        missing_frozen_amount.extend(target_account_asset_payload(
9868            Pubkey([0x51; 32]),
9869            Pubkey([0x52; 32]),
9870        ));
9871        missing_frozen_amount.push(RestrictionMode::FrozenAmount { amount: 1 }.mode_id());
9872        assert_parse_error(missing_frozen_amount, "payload truncated at frozen_amount");
9873
9874        assert_parse_error(
9875            make_restrict_action_data(
9876                target_account_asset_payload(Pubkey([0x53; 32]), Pubkey([0x54; 32])),
9877                &RestrictionMode::FrozenAmount { amount: 0 },
9878                RestrictionReason::TestnetDrill,
9879                None,
9880                None,
9881                None,
9882            ),
9883            "FrozenAmount restriction amount must be > 0",
9884        );
9885
9886        assert_parse_error(
9887            make_restrict_action_data(
9888                target_account_payload(Pubkey([0x55; 32])),
9889                &RestrictionMode::OutgoingOnly,
9890                RestrictionReason::StolenFunds,
9891                None,
9892                None,
9893                None,
9894            ),
9895            "requires evidence_hash or evidence_uri_hash",
9896        );
9897
9898        let mut unexpected_restrict_flags = vec![34u8, GOVERNANCE_ACTION_RESTRICT];
9899        unexpected_restrict_flags.extend(target_account_payload(Pubkey([0x56; 32])));
9900        unexpected_restrict_flags.push(RestrictionMode::OutgoingOnly.mode_id());
9901        unexpected_restrict_flags.push(RestrictionReason::TestnetDrill.as_u8());
9902        unexpected_restrict_flags.push(0x08);
9903        assert_parse_error(unexpected_restrict_flags, "unexpected flags 0x08");
9904
9905        let mut trailing_restrict = make_restrict_action_data(
9906            target_account_payload(Pubkey([0x57; 32])),
9907            &RestrictionMode::OutgoingOnly,
9908            RestrictionReason::TestnetDrill,
9909            None,
9910            None,
9911            None,
9912        );
9913        trailing_restrict.push(0xAA);
9914        assert_parse_error(trailing_restrict, "trailing bytes");
9915
9916        let mut empty_route_chain = vec![34u8, GOVERNANCE_ACTION_RESTRICT, 5];
9917        empty_route_chain.extend_from_slice(&0u16.to_le_bytes());
9918        assert_parse_error(empty_route_chain, "chain_id cannot be empty");
9919
9920        let mut invalid_route_utf8 = vec![34u8, GOVERNANCE_ACTION_RESTRICT, 5];
9921        invalid_route_utf8.extend_from_slice(&1u16.to_le_bytes());
9922        invalid_route_utf8.push(0xFF);
9923        assert_parse_error(invalid_route_utf8, "chain_id must be valid UTF-8");
9924
9925        let mut too_long_route = vec![34u8, GOVERNANCE_ACTION_RESTRICT, 5];
9926        too_long_route.extend_from_slice(&257u16.to_le_bytes());
9927        too_long_route.extend(std::iter::repeat_n(b'a', 257));
9928        assert_parse_error(too_long_route, "chain_id length 257 exceeds 256");
9929
9930        let mut unknown_module = vec![34u8, GOVERNANCE_ACTION_RESTRICT];
9931        unknown_module.extend([6u8, 99u8]);
9932        assert_parse_error(unknown_module, "unknown protocol module id 99");
9933
9934        let mut bad_lift_id = vec![34u8, GOVERNANCE_ACTION_LIFT_RESTRICTION];
9935        bad_lift_id.extend_from_slice(&0u64.to_le_bytes());
9936        bad_lift_id.push(RestrictionLiftReason::IncidentResolved.as_u8());
9937        assert_parse_error(bad_lift_id, "restriction_id must be greater than zero");
9938
9939        let mut bad_lift_reason = vec![34u8, GOVERNANCE_ACTION_LIFT_RESTRICTION];
9940        bad_lift_reason.extend_from_slice(&1u64.to_le_bytes());
9941        bad_lift_reason.push(99);
9942        assert_parse_error(bad_lift_reason, "unknown restriction lift reason 99");
9943
9944        let mut trailing_lift =
9945            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
9946        trailing_lift.push(0xAA);
9947        assert_parse_error(trailing_lift, "trailing bytes");
9948
9949        let mut bad_extend_id = vec![34u8, GOVERNANCE_ACTION_EXTEND_RESTRICTION];
9950        bad_extend_id.extend_from_slice(&0u64.to_le_bytes());
9951        bad_extend_id.push(0);
9952        assert_parse_error(bad_extend_id, "restriction_id must be greater than zero");
9953
9954        let mut bad_extend_flags = vec![34u8, GOVERNANCE_ACTION_EXTEND_RESTRICTION];
9955        bad_extend_flags.extend_from_slice(&1u64.to_le_bytes());
9956        bad_extend_flags.push(0x04);
9957        assert_parse_error(bad_extend_flags, "unexpected flags 0x04");
9958
9959        let mut truncated_extend = vec![34u8, GOVERNANCE_ACTION_EXTEND_RESTRICTION];
9960        truncated_extend.extend_from_slice(&1u64.to_le_bytes());
9961        truncated_extend.push(0x02);
9962        truncated_extend.extend_from_slice(&[0xAB; 31]);
9963        assert_parse_error(truncated_extend, "payload truncated at evidence_hash");
9964    }
9965
9966    #[test]
9967    fn test_restriction_governance_create_executes_through_proposal_lifecycle() {
9968        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
9969            setup_restriction_governance(2, 1);
9970        let validator = Pubkey([42u8; 32]);
9971        let target = Pubkey([0xA1; 32]);
9972        let expires_at_slot = SLOTS_PER_EPOCH * 2;
9973        let data = make_restrict_action_data(
9974            target_account_payload(target),
9975            &RestrictionMode::OutgoingOnly,
9976            RestrictionReason::TestnetDrill,
9977            None,
9978            None,
9979            Some(expires_at_slot),
9980        );
9981
9982        let result = process_governance_proposal(
9983            &processor,
9984            &alice_kp,
9985            alice,
9986            gov,
9987            data,
9988            genesis_hash,
9989            &validator,
9990        );
9991        assert!(
9992            result.success,
9993            "Restriction proposal should succeed: {:?}",
9994            result.error
9995        );
9996        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
9997        assert_eq!(proposal.approvals, vec![alice]);
9998        assert_eq!(proposal.execute_after_epoch, 1);
9999        assert!(!proposal.executed);
10000        assert!(state.get_restriction(1).unwrap().is_none());
10001
10002        let result =
10003            process_governance_control(&processor, &bob_kp, bob, 35, 1, genesis_hash, &validator);
10004        assert!(
10005            result.success,
10006            "Restriction approval should succeed: {:?}",
10007            result.error
10008        );
10009        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
10010        assert_eq!(proposal.approvals, vec![alice, bob]);
10011        assert!(!proposal.executed);
10012        assert!(state.get_restriction(1).unwrap().is_none());
10013
10014        let result = process_governance_control(
10015            &processor,
10016            &alice_kp,
10017            alice,
10018            36,
10019            1,
10020            genesis_hash,
10021            &validator,
10022        );
10023        assert!(!result.success);
10024        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
10025
10026        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
10027        let result = process_governance_control(
10028            &processor,
10029            &bob_kp,
10030            bob,
10031            36,
10032            1,
10033            fresh_blockhash,
10034            &validator,
10035        );
10036        assert!(
10037            result.success,
10038            "Restriction execution should succeed: {:?}",
10039            result.error
10040        );
10041
10042        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
10043        assert!(proposal.executed);
10044        let record = state.get_restriction(1).unwrap().unwrap();
10045        assert_eq!(record.id, 1);
10046        assert_eq!(record.target, RestrictionTarget::Account(target));
10047        assert_eq!(record.mode, RestrictionMode::OutgoingOnly);
10048        assert_eq!(record.status, RestrictionStatus::Active);
10049        assert_eq!(record.reason, RestrictionReason::TestnetDrill);
10050        assert_eq!(record.proposer, alice);
10051        assert_eq!(record.authority, gov);
10052        assert_eq!(record.approval_authority, None);
10053        assert_eq!(record.created_slot, SLOTS_PER_EPOCH);
10054        assert_eq!(record.created_epoch, 1);
10055        assert_eq!(record.expires_at_slot, Some(expires_at_slot));
10056        assert_eq!(record.supersedes, None);
10057    }
10058
10059    #[test]
10060    fn test_restriction_governance_lift_preserves_record_id() {
10061        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10062            setup_restriction_governance(2, 0);
10063        let validator = Pubkey([42u8; 32]);
10064        create_active_test_restriction(
10065            &processor,
10066            &state,
10067            &alice_kp,
10068            alice,
10069            &bob_kp,
10070            bob,
10071            gov,
10072            genesis_hash,
10073            &validator,
10074            Some(100),
10075        );
10076
10077        let data = make_lift_restriction_action_data(1, RestrictionLiftReason::FalsePositive);
10078        let result = process_governance_proposal(
10079            &processor,
10080            &alice_kp,
10081            alice,
10082            gov,
10083            data,
10084            genesis_hash,
10085            &validator,
10086        );
10087        assert!(
10088            result.success,
10089            "Lift proposal should succeed: {:?}",
10090            result.error
10091        );
10092        let result =
10093            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10094        assert!(
10095            result.success,
10096            "Lift approval should execute: {:?}",
10097            result.error
10098        );
10099
10100        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
10101        assert!(proposal.executed);
10102        let record = state.get_restriction(1).unwrap().unwrap();
10103        assert_eq!(record.id, 1);
10104        assert_eq!(record.status, RestrictionStatus::Lifted);
10105        assert_eq!(record.lifted_by, Some(gov));
10106        assert_eq!(record.lifted_slot, Some(0));
10107        assert_eq!(
10108            record.lift_reason,
10109            Some(RestrictionLiftReason::FalsePositive)
10110        );
10111        assert_eq!(state.get_restriction(2).unwrap(), None);
10112    }
10113
10114    #[test]
10115    fn test_restriction_governance_extend_supersedes_and_creates_successor() {
10116        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10117            setup_restriction_governance(2, 0);
10118        let validator = Pubkey([42u8; 32]);
10119        create_active_test_restriction(
10120            &processor,
10121            &state,
10122            &alice_kp,
10123            alice,
10124            &bob_kp,
10125            bob,
10126            gov,
10127            genesis_hash,
10128            &validator,
10129            Some(100),
10130        );
10131
10132        let replacement_evidence = Hash([0xE2; 32]);
10133        let data = make_extend_restriction_action_data(1, Some(200), Some(replacement_evidence));
10134        let result = process_governance_proposal(
10135            &processor,
10136            &alice_kp,
10137            alice,
10138            gov,
10139            data,
10140            genesis_hash,
10141            &validator,
10142        );
10143        assert!(
10144            result.success,
10145            "Extend proposal should succeed: {:?}",
10146            result.error
10147        );
10148        let result =
10149            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10150        assert!(
10151            result.success,
10152            "Extend approval should execute: {:?}",
10153            result.error
10154        );
10155
10156        let old_record = state.get_restriction(1).unwrap().unwrap();
10157        assert_eq!(old_record.status, RestrictionStatus::Superseded);
10158        assert_eq!(old_record.expires_at_slot, Some(100));
10159        assert_eq!(old_record.lifted_by, None);
10160
10161        let successor = state.get_restriction(2).unwrap().unwrap();
10162        assert_eq!(successor.id, 2);
10163        assert_eq!(successor.status, RestrictionStatus::Active);
10164        assert_eq!(successor.target, old_record.target);
10165        assert_eq!(successor.mode, old_record.mode);
10166        assert_eq!(successor.reason, old_record.reason);
10167        assert_eq!(successor.evidence_hash, Some(replacement_evidence));
10168        assert_eq!(successor.proposer, alice);
10169        assert_eq!(successor.authority, gov);
10170        assert_eq!(successor.created_slot, 0);
10171        assert_eq!(successor.created_epoch, 0);
10172        assert_eq!(successor.expires_at_slot, Some(200));
10173        assert_eq!(successor.supersedes, Some(1));
10174    }
10175
10176    #[test]
10177    fn test_restriction_governance_cancel_leaves_state_unmutated() {
10178        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10179            setup_restriction_governance(2, 0);
10180        let validator = Pubkey([42u8; 32]);
10181        let data = make_restrict_action_data(
10182            target_account_payload(Pubkey([0xB1; 32])),
10183            &RestrictionMode::IncomingOnly,
10184            RestrictionReason::TestnetDrill,
10185            None,
10186            None,
10187            Some(100),
10188        );
10189        let result = process_governance_proposal(
10190            &processor,
10191            &alice_kp,
10192            alice,
10193            gov,
10194            data,
10195            genesis_hash,
10196            &validator,
10197        );
10198        assert!(
10199            result.success,
10200            "Restriction proposal should succeed: {:?}",
10201            result.error
10202        );
10203        assert!(state.get_restriction(1).unwrap().is_none());
10204
10205        let result =
10206            process_governance_control(&processor, &bob_kp, bob, 37, 1, genesis_hash, &validator);
10207        assert!(
10208            result.success,
10209            "Cancellation should succeed: {:?}",
10210            result.error
10211        );
10212
10213        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
10214        assert!(proposal.cancelled);
10215        assert!(!proposal.executed);
10216        assert!(state.get_restriction(1).unwrap().is_none());
10217    }
10218
10219    #[test]
10220    fn test_restriction_governance_rejects_invalid_transitions_atomically() {
10221        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10222            setup_restriction_governance(2, 0);
10223        let validator = Pubkey([42u8; 32]);
10224        create_active_test_restriction(
10225            &processor,
10226            &state,
10227            &alice_kp,
10228            alice,
10229            &bob_kp,
10230            bob,
10231            gov,
10232            genesis_hash,
10233            &validator,
10234            Some(100),
10235        );
10236
10237        let data = make_extend_restriction_action_data(1, Some(100), None);
10238        let result = process_governance_proposal(
10239            &processor,
10240            &alice_kp,
10241            alice,
10242            gov,
10243            data,
10244            genesis_hash,
10245            &validator,
10246        );
10247        assert!(
10248            result.success,
10249            "Invalid extend proposal should still be stored: {:?}",
10250            result.error
10251        );
10252        let result =
10253            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10254        assert!(!result.success);
10255        assert!(result
10256            .error
10257            .as_deref()
10258            .unwrap_or("")
10259            .contains("must be greater than current expiry"));
10260
10261        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
10262        assert_eq!(proposal.approvals, vec![alice]);
10263        assert!(!proposal.executed);
10264        assert_eq!(
10265            state.get_restriction(1).unwrap().unwrap().status,
10266            RestrictionStatus::Active
10267        );
10268        assert!(state.get_restriction(2).unwrap().is_none());
10269
10270        let fresh_blockhash = advance_test_slot(&state, 100);
10271        let data = make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
10272        let result = process_governance_proposal(
10273            &processor,
10274            &alice_kp,
10275            alice,
10276            gov,
10277            data,
10278            fresh_blockhash,
10279            &validator,
10280        );
10281        assert!(
10282            result.success,
10283            "Expired lift proposal should still be stored: {:?}",
10284            result.error
10285        );
10286        let result = process_governance_control(
10287            &processor,
10288            &bob_kp,
10289            bob,
10290            35,
10291            3,
10292            fresh_blockhash,
10293            &validator,
10294        );
10295        assert!(!result.success);
10296        assert!(result.error.as_deref().unwrap_or("").contains("not active"));
10297
10298        let proposal = state.get_governance_proposal(3).unwrap().unwrap();
10299        assert_eq!(proposal.approvals, vec![alice]);
10300        assert!(!proposal.executed);
10301        let record = state.get_restriction(1).unwrap().unwrap();
10302        assert_eq!(record.status, RestrictionStatus::Active);
10303        assert_eq!(
10304            state
10305                .get_effective_restriction_record(1, 100)
10306                .unwrap()
10307                .unwrap()
10308                .effective_status,
10309            RestrictionStatus::Expired
10310        );
10311        assert!(state.get_restriction(2).unwrap().is_none());
10312    }
10313
10314    #[test]
10315    fn test_restriction_governance_extend_rejects_indefinite_records() {
10316        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10317            setup_restriction_governance(2, 0);
10318        let validator = Pubkey([42u8; 32]);
10319        create_active_test_restriction(
10320            &processor,
10321            &state,
10322            &alice_kp,
10323            alice,
10324            &bob_kp,
10325            bob,
10326            gov,
10327            genesis_hash,
10328            &validator,
10329            None,
10330        );
10331
10332        let data = make_extend_restriction_action_data(1, Some(200), None);
10333        let result = process_governance_proposal(
10334            &processor,
10335            &alice_kp,
10336            alice,
10337            gov,
10338            data,
10339            genesis_hash,
10340            &validator,
10341        );
10342        assert!(
10343            result.success,
10344            "Indefinite extend proposal should still be stored: {:?}",
10345            result.error
10346        );
10347        let result =
10348            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10349        assert!(!result.success);
10350        assert!(result
10351            .error
10352            .as_deref()
10353            .unwrap_or("")
10354            .contains("has no expiry to extend"));
10355
10356        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
10357        assert_eq!(proposal.approvals, vec![alice]);
10358        assert!(!proposal.executed);
10359        assert_eq!(
10360            state.get_restriction(1).unwrap().unwrap().status,
10361            RestrictionStatus::Active
10362        );
10363        assert!(state.get_restriction(2).unwrap().is_none());
10364    }
10365
10366    #[test]
10367    fn test_restriction_governance_lifecycle_events_use_stored_record_metadata() {
10368        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10369            setup_restriction_governance(2, 0);
10370        let validator = Pubkey([42u8; 32]);
10371        create_active_test_restriction(
10372            &processor,
10373            &state,
10374            &alice_kp,
10375            alice,
10376            &bob_kp,
10377            bob,
10378            gov,
10379            genesis_hash,
10380            &validator,
10381            Some(100),
10382        );
10383
10384        let events = state
10385            .get_events_by_program(&SYSTEM_PROGRAM_ID, 20, None)
10386            .unwrap();
10387        let created = find_system_event(&events, "RestrictionCreated", 1);
10388        assert_eq!(
10389            created.data.get("action").map(String::as_str),
10390            Some("restrict")
10391        );
10392        assert_eq!(created.data.get("actor"), Some(&bob.to_base58()));
10393        assert_eq!(created.data.get("authority"), Some(&gov.to_base58()));
10394        assert_eq!(created.data.get("proposer"), Some(&alice.to_base58()));
10395        assert_eq!(created.data.get("approvals").map(String::as_str), Some("2"));
10396        assert_eq!(created.data.get("threshold").map(String::as_str), Some("2"));
10397        assert_eq!(
10398            created.data.get("executed").map(String::as_str),
10399            Some("true")
10400        );
10401        assert_eq!(
10402            created.data.get("restriction_id").map(String::as_str),
10403            Some("1")
10404        );
10405        assert_eq!(
10406            created.data.get("restriction_status").map(String::as_str),
10407            Some("active")
10408        );
10409        assert_eq!(
10410            created
10411                .data
10412                .get("restriction_target_type")
10413                .map(String::as_str),
10414            Some("account")
10415        );
10416        assert_eq!(
10417            created.data.get("restriction_mode").map(String::as_str),
10418            Some("outgoing_only")
10419        );
10420        assert_eq!(
10421            created.data.get("restriction_reason").map(String::as_str),
10422            Some("testnet_drill")
10423        );
10424        assert_eq!(
10425            created.data.get("created_slot").map(String::as_str),
10426            Some("0")
10427        );
10428        assert_eq!(
10429            created.data.get("created_epoch").map(String::as_str),
10430            Some("0")
10431        );
10432        assert_eq!(
10433            created.data.get("expires_at_slot").map(String::as_str),
10434            Some("100")
10435        );
10436
10437        let extend_data = make_extend_restriction_action_data(1, Some(200), None);
10438        let result = process_governance_proposal(
10439            &processor,
10440            &alice_kp,
10441            alice,
10442            gov,
10443            extend_data,
10444            genesis_hash,
10445            &validator,
10446        );
10447        assert!(
10448            result.success,
10449            "Extend proposal should succeed: {:?}",
10450            result.error
10451        );
10452        let result =
10453            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10454        assert!(
10455            result.success,
10456            "Extend approval should execute: {:?}",
10457            result.error
10458        );
10459
10460        let events = state
10461            .get_events_by_program(&SYSTEM_PROGRAM_ID, 40, None)
10462            .unwrap();
10463        let extended = find_system_event(&events, "RestrictionExtended", 2);
10464        assert_eq!(
10465            extended.data.get("restriction_id").map(String::as_str),
10466            Some("2")
10467        );
10468        assert_eq!(
10469            extended.data.get("restriction_status").map(String::as_str),
10470            Some("active")
10471        );
10472        assert_eq!(
10473            extended.data.get("supersedes").map(String::as_str),
10474            Some("1")
10475        );
10476        assert_eq!(
10477            extended.data.get("expires_at_slot").map(String::as_str),
10478            Some("200")
10479        );
10480        assert_eq!(
10481            extended.data.get("restriction_mode").map(String::as_str),
10482            Some("outgoing_only")
10483        );
10484
10485        let lift_data =
10486            make_lift_restriction_action_data(2, RestrictionLiftReason::IncidentResolved);
10487        let result = process_governance_proposal(
10488            &processor,
10489            &alice_kp,
10490            alice,
10491            gov,
10492            lift_data,
10493            genesis_hash,
10494            &validator,
10495        );
10496        assert!(
10497            result.success,
10498            "Lift proposal should succeed: {:?}",
10499            result.error
10500        );
10501        let result =
10502            process_governance_control(&processor, &bob_kp, bob, 35, 3, genesis_hash, &validator);
10503        assert!(
10504            result.success,
10505            "Lift approval should execute: {:?}",
10506            result.error
10507        );
10508
10509        let events = state
10510            .get_events_by_program(&SYSTEM_PROGRAM_ID, 60, None)
10511            .unwrap();
10512        let lifted = find_system_event(&events, "RestrictionLifted", 3);
10513        assert_eq!(
10514            lifted.data.get("restriction_id").map(String::as_str),
10515            Some("2")
10516        );
10517        assert_eq!(
10518            lifted.data.get("restriction_status").map(String::as_str),
10519            Some("lifted")
10520        );
10521        assert_eq!(lifted.data.get("lifted_by"), Some(&gov.to_base58()));
10522        assert_eq!(
10523            lifted.data.get("lifted_slot").map(String::as_str),
10524            Some("0")
10525        );
10526        assert_eq!(
10527            lifted.data.get("lift_reason").map(String::as_str),
10528            Some("incident_resolved")
10529        );
10530        assert_eq!(
10531            lifted.data.get("restriction_reason").map(String::as_str),
10532            Some("testnet_drill")
10533        );
10534    }
10535
10536    #[test]
10537    fn test_restriction_governance_failed_execution_emits_no_lifecycle_event() {
10538        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10539            setup_restriction_governance(2, 0);
10540        let validator = Pubkey([42u8; 32]);
10541        create_active_test_restriction(
10542            &processor,
10543            &state,
10544            &alice_kp,
10545            alice,
10546            &bob_kp,
10547            bob,
10548            gov,
10549            genesis_hash,
10550            &validator,
10551            Some(100),
10552        );
10553
10554        let invalid_extend_data = make_extend_restriction_action_data(1, Some(100), None);
10555        let result = process_governance_proposal(
10556            &processor,
10557            &alice_kp,
10558            alice,
10559            gov,
10560            invalid_extend_data,
10561            genesis_hash,
10562            &validator,
10563        );
10564        assert!(
10565            result.success,
10566            "Invalid extend proposal should be stored: {:?}",
10567            result.error
10568        );
10569        let result =
10570            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10571        assert!(!result.success);
10572
10573        let events = state
10574            .get_events_by_program(&SYSTEM_PROGRAM_ID, 40, None)
10575            .unwrap();
10576        assert!(!events.iter().any(|event| {
10577            event.name == "RestrictionExtended"
10578                && event.data.get("proposal_id").map(String::as_str) == Some("2")
10579        }));
10580    }
10581
10582    #[test]
10583    fn test_restriction_governance_main_authority_remains_higher_authority_for_split_targets() {
10584        let (processor, state, alice_kp, alice, _bob_kp, bob, gov, genesis_hash) =
10585            setup_restriction_governance(2, 5);
10586        let validator = Pubkey([42u8; 32]);
10587        configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
10588        configure_oracle_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
10589
10590        let bridge_data = make_restrict_action_data(
10591            target_bridge_route_payload("neo-x-testnet", "USDT"),
10592            &RestrictionMode::RoutePaused,
10593            RestrictionReason::TestnetDrill,
10594            None,
10595            None,
10596            Some(SLOTS_PER_EPOCH * 4),
10597        );
10598        let result = process_governance_proposal(
10599            &processor,
10600            &alice_kp,
10601            alice,
10602            gov,
10603            bridge_data,
10604            genesis_hash,
10605            &validator,
10606        );
10607        assert!(
10608            result.success,
10609            "Main governance bridge restriction proposal should succeed: {:?}",
10610            result.error
10611        );
10612        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
10613        assert_eq!(proposal.authority, gov);
10614        assert_eq!(proposal.approval_authority, None);
10615        assert_eq!(proposal.execute_after_epoch, 5);
10616
10617        let oracle_data = make_restrict_action_data(
10618            target_protocol_module_payload(ProtocolModuleId::Oracle),
10619            &RestrictionMode::ProtocolPaused,
10620            RestrictionReason::TestnetDrill,
10621            None,
10622            None,
10623            Some(SLOTS_PER_EPOCH * 4),
10624        );
10625        let result = process_governance_proposal(
10626            &processor,
10627            &alice_kp,
10628            alice,
10629            gov,
10630            oracle_data,
10631            genesis_hash,
10632            &validator,
10633        );
10634        assert!(
10635            result.success,
10636            "Main governance oracle restriction proposal should succeed: {:?}",
10637            result.error
10638        );
10639        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
10640        assert_eq!(proposal.authority, gov);
10641        assert_eq!(proposal.approval_authority, None);
10642        assert_eq!(proposal.execute_after_epoch, 5);
10643    }
10644
10645    #[test]
10646    fn test_restriction_governance_split_roles_route_scoped_creates() {
10647        let (processor, state, alice_kp, alice, _bob_kp, bob, gov, genesis_hash) =
10648            setup_restriction_governance(2, 5);
10649        let validator = Pubkey([42u8; 32]);
10650        let bridge_authority =
10651            configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
10652        let oracle_authority =
10653            configure_oracle_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
10654
10655        let route_data = make_restrict_action_data(
10656            target_bridge_route_payload("neo-x-testnet", "USDT"),
10657            &RestrictionMode::RoutePaused,
10658            RestrictionReason::TestnetDrill,
10659            None,
10660            None,
10661            Some(SLOTS_PER_EPOCH * 4),
10662        );
10663        let result = process_governance_proposal(
10664            &processor,
10665            &alice_kp,
10666            alice,
10667            bridge_authority,
10668            route_data,
10669            genesis_hash,
10670            &validator,
10671        );
10672        assert!(
10673            result.success,
10674            "Bridge route restriction should route to bridge split authority: {:?}",
10675            result.error
10676        );
10677        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
10678        assert_eq!(proposal.authority, gov);
10679        assert_eq!(proposal.approval_authority, Some(bridge_authority));
10680        assert_eq!(proposal.execute_after_epoch, 1);
10681
10682        let bridge_module_data = make_restrict_action_data(
10683            target_protocol_module_payload(ProtocolModuleId::Bridge),
10684            &RestrictionMode::ProtocolPaused,
10685            RestrictionReason::TestnetDrill,
10686            None,
10687            None,
10688            Some(SLOTS_PER_EPOCH * 4),
10689        );
10690        let result = process_governance_proposal(
10691            &processor,
10692            &alice_kp,
10693            alice,
10694            bridge_authority,
10695            bridge_module_data,
10696            genesis_hash,
10697            &validator,
10698        );
10699        assert!(
10700            result.success,
10701            "Bridge protocol restriction should route to bridge split authority: {:?}",
10702            result.error
10703        );
10704        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
10705        assert_eq!(proposal.authority, gov);
10706        assert_eq!(proposal.approval_authority, Some(bridge_authority));
10707        assert_eq!(proposal.execute_after_epoch, 1);
10708
10709        let oracle_data = make_restrict_action_data(
10710            target_protocol_module_payload(ProtocolModuleId::Oracle),
10711            &RestrictionMode::ProtocolPaused,
10712            RestrictionReason::TestnetDrill,
10713            None,
10714            None,
10715            Some(SLOTS_PER_EPOCH * 4),
10716        );
10717        let result = process_governance_proposal(
10718            &processor,
10719            &alice_kp,
10720            alice,
10721            oracle_authority,
10722            oracle_data,
10723            genesis_hash,
10724            &validator,
10725        );
10726        assert!(
10727            result.success,
10728            "Oracle protocol restriction should route to oracle split authority: {:?}",
10729            result.error
10730        );
10731        let proposal = state.get_governance_proposal(3).unwrap().unwrap();
10732        assert_eq!(proposal.authority, gov);
10733        assert_eq!(proposal.approval_authority, Some(oracle_authority));
10734        assert_eq!(proposal.execute_after_epoch, 1);
10735    }
10736
10737    #[test]
10738    fn test_restriction_governance_rejects_wrong_split_routing() {
10739        let (processor, state, alice_kp, alice, _bob_kp, bob, gov, genesis_hash) =
10740            setup_restriction_governance(2, 5);
10741        let validator = Pubkey([42u8; 32]);
10742        let bridge_authority =
10743            configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
10744        let oracle_authority =
10745            configure_oracle_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
10746
10747        let oracle_data = make_restrict_action_data(
10748            target_protocol_module_payload(ProtocolModuleId::Oracle),
10749            &RestrictionMode::ProtocolPaused,
10750            RestrictionReason::TestnetDrill,
10751            None,
10752            None,
10753            Some(SLOTS_PER_EPOCH * 4),
10754        );
10755        let result = process_governance_proposal(
10756            &processor,
10757            &alice_kp,
10758            alice,
10759            bridge_authority,
10760            oracle_data,
10761            genesis_hash,
10762            &validator,
10763        );
10764        assert!(!result.success);
10765        assert!(result
10766            .error
10767            .as_deref()
10768            .unwrap_or("")
10769            .contains("Governance action authority account mismatch"));
10770
10771        let route_data = make_restrict_action_data(
10772            target_bridge_route_payload("neo-x-testnet", "USDT"),
10773            &RestrictionMode::RoutePaused,
10774            RestrictionReason::TestnetDrill,
10775            None,
10776            None,
10777            Some(SLOTS_PER_EPOCH * 4),
10778        );
10779        let result = process_governance_proposal(
10780            &processor,
10781            &alice_kp,
10782            alice,
10783            oracle_authority,
10784            route_data,
10785            genesis_hash,
10786            &validator,
10787        );
10788        assert!(!result.success);
10789        assert!(result
10790            .error
10791            .as_deref()
10792            .unwrap_or("")
10793            .contains("Governance action authority account mismatch"));
10794    }
10795
10796    #[test]
10797    fn test_restriction_governance_guardian_can_create_and_lift_temporary_account_restriction() {
10798        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
10799            setup_restriction_governance(2, 5);
10800        let validator = Pubkey([42u8; 32]);
10801        let guardian_authority =
10802            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
10803
10804        let record = create_active_guardian_test_restriction(
10805            &processor,
10806            &state,
10807            &alice_kp,
10808            alice,
10809            &bob_kp,
10810            bob,
10811            guardian_authority,
10812            genesis_hash,
10813            &validator,
10814            1,
10815            1,
10816            target_account_payload(Pubkey([0xD1; 32])),
10817            RestrictionMode::OutgoingOnly,
10818            100,
10819        );
10820        assert_eq!(record.authority, gov);
10821        assert_eq!(record.created_slot, 0);
10822
10823        let lift_data =
10824            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
10825        let result = process_governance_proposal(
10826            &processor,
10827            &alice_kp,
10828            alice,
10829            guardian_authority,
10830            lift_data,
10831            genesis_hash,
10832            &validator,
10833        );
10834        assert!(
10835            result.success,
10836            "Guardian should be able to propose lift for its restriction: {:?}",
10837            result.error
10838        );
10839        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
10840        assert_eq!(proposal.authority, gov);
10841        assert_eq!(proposal.approval_authority, Some(guardian_authority));
10842        assert_eq!(proposal.execute_after_epoch, 0);
10843
10844        let result =
10845            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
10846        assert!(
10847            result.success,
10848            "Guardian lift approval should execute: {:?}",
10849            result.error
10850        );
10851        let lifted = state.get_restriction(1).unwrap().unwrap();
10852        assert_eq!(lifted.status, RestrictionStatus::Lifted);
10853        assert_eq!(
10854            lifted.lift_reason,
10855            Some(RestrictionLiftReason::IncidentResolved)
10856        );
10857        assert_eq!(lifted.approval_authority, Some(guardian_authority));
10858    }
10859
10860    #[test]
10861    fn test_restriction_governance_guardian_rejects_unbounded_and_disallowed_restrictions() {
10862        let (processor, state, alice_kp, alice, _bob_kp, bob, gov, genesis_hash) =
10863            setup_restriction_governance(2, 5);
10864        let validator = Pubkey([42u8; 32]);
10865        let guardian_authority =
10866            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
10867
10868        let unbounded_data = make_restrict_action_data(
10869            target_account_payload(Pubkey([0xD2; 32])),
10870            &RestrictionMode::OutgoingOnly,
10871            RestrictionReason::TestnetDrill,
10872            None,
10873            None,
10874            None,
10875        );
10876        let result = process_governance_proposal(
10877            &processor,
10878            &alice_kp,
10879            alice,
10880            guardian_authority,
10881            unbounded_data,
10882            genesis_hash,
10883            &validator,
10884        );
10885        assert!(!result.success);
10886        assert!(result
10887            .error
10888            .as_deref()
10889            .unwrap_or("")
10890            .contains("must include expires_at_slot"));
10891
10892        let account_freeze_data = make_restrict_action_data(
10893            target_account_payload(Pubkey([0xD3; 32])),
10894            &RestrictionMode::Bidirectional,
10895            RestrictionReason::TestnetDrill,
10896            None,
10897            None,
10898            Some(100),
10899        );
10900        let result = process_governance_proposal(
10901            &processor,
10902            &alice_kp,
10903            alice,
10904            guardian_authority,
10905            account_freeze_data,
10906            genesis_hash,
10907            &validator,
10908        );
10909        assert!(!result.success);
10910        assert!(result
10911            .error
10912            .as_deref()
10913            .unwrap_or("")
10914            .contains("target/mode is not allowed"));
10915
10916        let contract_terminated_data = make_restrict_action_data(
10917            target_pubkey_payload(3, Pubkey([0xD4; 32])),
10918            &RestrictionMode::Terminated,
10919            RestrictionReason::TestnetDrill,
10920            None,
10921            None,
10922            Some(100),
10923        );
10924        let result = process_governance_proposal(
10925            &processor,
10926            &alice_kp,
10927            alice,
10928            guardian_authority,
10929            contract_terminated_data,
10930            genesis_hash,
10931            &validator,
10932        );
10933        assert!(!result.success);
10934        assert!(result
10935            .error
10936            .as_deref()
10937            .unwrap_or("")
10938            .contains("target/mode is not allowed"));
10939
10940        let native_pause_data = make_restrict_action_data(
10941            target_protocol_module_payload(ProtocolModuleId::Native),
10942            &RestrictionMode::ProtocolPaused,
10943            RestrictionReason::TestnetDrill,
10944            None,
10945            None,
10946            Some(100),
10947        );
10948        let result = process_governance_proposal(
10949            &processor,
10950            &alice_kp,
10951            alice,
10952            guardian_authority,
10953            native_pause_data,
10954            genesis_hash,
10955            &validator,
10956        );
10957        assert!(!result.success);
10958        assert!(result
10959            .error
10960            .as_deref()
10961            .unwrap_or("")
10962            .contains("target/mode is not allowed"));
10963    }
10964
10965    #[test]
10966    fn test_restriction_governance_guardian_ttl_cap_enforced() {
10967        let (processor, state, alice_kp, alice, _bob_kp, bob, gov, genesis_hash) =
10968            setup_restriction_governance(2, 5);
10969        let validator = Pubkey([42u8; 32]);
10970        let guardian_authority =
10971            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
10972
10973        let data = make_restrict_action_data(
10974            target_account_payload(Pubkey([0xD5; 32])),
10975            &RestrictionMode::OutgoingOnly,
10976            RestrictionReason::TestnetDrill,
10977            None,
10978            None,
10979            Some(GUARDIAN_RESTRICTION_MAX_SLOTS + 1),
10980        );
10981        let result = process_governance_proposal(
10982            &processor,
10983            &alice_kp,
10984            alice,
10985            guardian_authority,
10986            data,
10987            genesis_hash,
10988            &validator,
10989        );
10990        assert!(!result.success);
10991        assert!(result
10992            .error
10993            .as_deref()
10994            .unwrap_or("")
10995            .contains("exceeds guardian TTL cap"));
10996    }
10997
10998    #[test]
10999    fn test_restriction_governance_guardian_can_create_code_hash_and_contract_blocks() {
11000        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
11001            setup_restriction_governance(2, 5);
11002        let validator = Pubkey([42u8; 32]);
11003        let guardian_authority =
11004            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
11005
11006        let code_record = create_active_guardian_test_restriction(
11007            &processor,
11008            &state,
11009            &alice_kp,
11010            alice,
11011            &bob_kp,
11012            bob,
11013            guardian_authority,
11014            genesis_hash,
11015            &validator,
11016            1,
11017            1,
11018            target_code_hash_payload(Hash([0xD6; 32])),
11019            RestrictionMode::DeployBlocked,
11020            100,
11021        );
11022        assert!(matches!(code_record.target, RestrictionTarget::CodeHash(_)));
11023
11024        let contract_target = Pubkey([0xD7; 32]);
11025        deploy_fake_contract(&state, alice, contract_target);
11026        let contract_record = create_active_guardian_test_restriction(
11027            &processor,
11028            &state,
11029            &alice_kp,
11030            alice,
11031            &bob_kp,
11032            bob,
11033            guardian_authority,
11034            genesis_hash,
11035            &validator,
11036            2,
11037            2,
11038            target_pubkey_payload(3, contract_target),
11039            RestrictionMode::StateChangingBlocked,
11040            120,
11041        );
11042        assert!(matches!(
11043            contract_record.target,
11044            RestrictionTarget::Contract(_)
11045        ));
11046    }
11047
11048    #[test]
11049    fn test_restriction_governance_guardian_can_extend_own_temporary_restriction_once() {
11050        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
11051            setup_restriction_governance(2, 5);
11052        let validator = Pubkey([42u8; 32]);
11053        let guardian_authority =
11054            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
11055
11056        create_active_guardian_test_restriction(
11057            &processor,
11058            &state,
11059            &alice_kp,
11060            alice,
11061            &bob_kp,
11062            bob,
11063            guardian_authority,
11064            genesis_hash,
11065            &validator,
11066            1,
11067            1,
11068            target_account_payload(Pubkey([0xD8; 32])),
11069            RestrictionMode::OutgoingOnly,
11070            100,
11071        );
11072
11073        let over_cap_data =
11074            make_extend_restriction_action_data(1, Some(GUARDIAN_RESTRICTION_MAX_SLOTS + 1), None);
11075        let result = process_governance_proposal(
11076            &processor,
11077            &alice_kp,
11078            alice,
11079            guardian_authority,
11080            over_cap_data,
11081            genesis_hash,
11082            &validator,
11083        );
11084        assert!(!result.success);
11085        assert!(result
11086            .error
11087            .as_deref()
11088            .unwrap_or("")
11089            .contains("exceeds guardian TTL cap"));
11090
11091        let extend_data = make_extend_restriction_action_data(1, Some(200), None);
11092        let result = process_governance_proposal(
11093            &processor,
11094            &alice_kp,
11095            alice,
11096            guardian_authority,
11097            extend_data,
11098            genesis_hash,
11099            &validator,
11100        );
11101        assert!(
11102            result.success,
11103            "Guardian should be able to propose one extension: {:?}",
11104            result.error
11105        );
11106        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
11107        assert_eq!(proposal.authority, gov);
11108        assert_eq!(proposal.approval_authority, Some(guardian_authority));
11109
11110        let result =
11111            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
11112        assert!(
11113            result.success,
11114            "Guardian extension approval should execute: {:?}",
11115            result.error
11116        );
11117        let old_record = state.get_restriction(1).unwrap().unwrap();
11118        assert_eq!(old_record.status, RestrictionStatus::Superseded);
11119        let successor = state.get_restriction(2).unwrap().unwrap();
11120        assert_eq!(successor.status, RestrictionStatus::Active);
11121        assert_eq!(successor.approval_authority, Some(guardian_authority));
11122        assert_eq!(successor.supersedes, Some(1));
11123        assert_eq!(successor.expires_at_slot, Some(200));
11124
11125        let main_lift_data =
11126            make_lift_restriction_action_data(2, RestrictionLiftReason::IncidentResolved);
11127        let result = process_governance_proposal(
11128            &processor,
11129            &alice_kp,
11130            alice,
11131            gov,
11132            main_lift_data,
11133            genesis_hash,
11134            &validator,
11135        );
11136        assert!(
11137            result.success,
11138            "Main governance should be able to propose guardian-created lift: {:?}",
11139            result.error
11140        );
11141        let proposal = state.get_governance_proposal(3).unwrap().unwrap();
11142        assert_eq!(proposal.authority, gov);
11143        assert_eq!(proposal.approval_authority, None);
11144    }
11145
11146    #[test]
11147    fn test_restriction_governance_guardian_rejects_second_extension_and_non_owned_lift() {
11148        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
11149            setup_restriction_governance(2, 5);
11150        let validator = Pubkey([42u8; 32]);
11151        let guardian_authority =
11152            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
11153
11154        create_active_guardian_test_restriction(
11155            &processor,
11156            &state,
11157            &alice_kp,
11158            alice,
11159            &bob_kp,
11160            bob,
11161            guardian_authority,
11162            genesis_hash,
11163            &validator,
11164            1,
11165            1,
11166            target_account_payload(Pubkey([0xD9; 32])),
11167            RestrictionMode::OutgoingOnly,
11168            100,
11169        );
11170        let extend_data = make_extend_restriction_action_data(1, Some(200), None);
11171        let result = process_governance_proposal(
11172            &processor,
11173            &alice_kp,
11174            alice,
11175            guardian_authority,
11176            extend_data,
11177            genesis_hash,
11178            &validator,
11179        );
11180        assert!(
11181            result.success,
11182            "First guardian extension should be proposed: {:?}",
11183            result.error
11184        );
11185        let result =
11186            process_governance_control(&processor, &bob_kp, bob, 35, 2, genesis_hash, &validator);
11187        assert!(
11188            result.success,
11189            "First guardian extension should execute: {:?}",
11190            result.error
11191        );
11192
11193        let second_extend_data = make_extend_restriction_action_data(2, Some(300), None);
11194        let result = process_governance_proposal(
11195            &processor,
11196            &alice_kp,
11197            alice,
11198            guardian_authority,
11199            second_extend_data,
11200            genesis_hash,
11201            &validator,
11202        );
11203        assert!(!result.success);
11204        assert!(result.error.as_deref().unwrap_or("").contains("only once"));
11205
11206        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
11207            setup_restriction_governance(2, 0);
11208        let guardian_authority =
11209            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
11210        create_active_test_restriction(
11211            &processor,
11212            &state,
11213            &alice_kp,
11214            alice,
11215            &bob_kp,
11216            bob,
11217            gov,
11218            genesis_hash,
11219            &validator,
11220            Some(100),
11221        );
11222
11223        let lift_data =
11224            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
11225        let result = process_governance_proposal(
11226            &processor,
11227            &alice_kp,
11228            alice,
11229            guardian_authority,
11230            lift_data,
11231            genesis_hash,
11232            &validator,
11233        );
11234        assert!(!result.success);
11235        assert!(result
11236            .error
11237            .as_deref()
11238            .unwrap_or("")
11239            .contains("only lift or extend restrictions it created"));
11240    }
11241
11242    #[test]
11243    fn test_restriction_governance_split_created_records_allow_stored_and_main_followups() {
11244        let (processor, state, alice_kp, alice, bob_kp, bob, gov, genesis_hash) =
11245            setup_restriction_governance(2, 5);
11246        let validator = Pubkey([42u8; 32]);
11247        let bridge_authority =
11248            configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
11249        let fresh_blockhash = create_active_bridge_split_restriction(
11250            &processor,
11251            &state,
11252            &alice_kp,
11253            alice,
11254            &bob_kp,
11255            bob,
11256            bridge_authority,
11257            genesis_hash,
11258            &validator,
11259        );
11260
11261        let split_lift_data =
11262            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
11263        let result = process_governance_proposal(
11264            &processor,
11265            &alice_kp,
11266            alice,
11267            bridge_authority,
11268            split_lift_data,
11269            fresh_blockhash,
11270            &validator,
11271        );
11272        assert!(
11273            result.success,
11274            "Stored split authority should be able to propose lift: {:?}",
11275            result.error
11276        );
11277        let proposal = state.get_governance_proposal(2).unwrap().unwrap();
11278        assert_eq!(proposal.authority, gov);
11279        assert_eq!(proposal.approval_authority, Some(bridge_authority));
11280
11281        let split_extend_data =
11282            make_extend_restriction_action_data(1, Some(SLOTS_PER_EPOCH * 5), None);
11283        let result = process_governance_proposal(
11284            &processor,
11285            &alice_kp,
11286            alice,
11287            bridge_authority,
11288            split_extend_data,
11289            fresh_blockhash,
11290            &validator,
11291        );
11292        assert!(
11293            result.success,
11294            "Stored split authority should be able to propose extension: {:?}",
11295            result.error
11296        );
11297        let proposal = state.get_governance_proposal(3).unwrap().unwrap();
11298        assert_eq!(proposal.authority, gov);
11299        assert_eq!(proposal.approval_authority, Some(bridge_authority));
11300
11301        let main_lift_data =
11302            make_lift_restriction_action_data(1, RestrictionLiftReason::IncidentResolved);
11303        let result = process_governance_proposal(
11304            &processor,
11305            &alice_kp,
11306            alice,
11307            gov,
11308            main_lift_data,
11309            fresh_blockhash,
11310            &validator,
11311        );
11312        assert!(
11313            result.success,
11314            "Main governance should be able to propose split-created lift: {:?}",
11315            result.error
11316        );
11317        let proposal = state.get_governance_proposal(4).unwrap().unwrap();
11318        assert_eq!(proposal.authority, gov);
11319        assert_eq!(proposal.approval_authority, None);
11320
11321        let main_extend_data =
11322            make_extend_restriction_action_data(1, Some(SLOTS_PER_EPOCH * 5), None);
11323        let result = process_governance_proposal(
11324            &processor,
11325            &alice_kp,
11326            alice,
11327            gov,
11328            main_extend_data,
11329            fresh_blockhash,
11330            &validator,
11331        );
11332        assert!(
11333            result.success,
11334            "Main governance should be able to propose split-created extension: {:?}",
11335            result.error
11336        );
11337        let proposal = state.get_governance_proposal(5).unwrap().unwrap();
11338        assert_eq!(proposal.authority, gov);
11339        assert_eq!(proposal.approval_authority, None);
11340    }
11341
11342    #[test]
11343    fn test_upgrade_timelock_set_and_stage() {
11344        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11345        let validator = Pubkey([42u8; 32]);
11346
11347        // Deploy contract
11348        let contract_addr = deploy_test_contract(
11349            &processor,
11350            &state,
11351            &alice_kp,
11352            alice,
11353            genesis_hash,
11354            &validator,
11355        );
11356
11357        // Set 3-epoch timelock
11358        let result = submit_contract_ix(
11359            &processor,
11360            &alice_kp,
11361            vec![alice, contract_addr],
11362            crate::ContractInstruction::SetUpgradeTimelock { epochs: 3 },
11363            genesis_hash,
11364            &validator,
11365        );
11366        assert!(
11367            result.success,
11368            "SetUpgradeTimelock should succeed: {:?}",
11369            result.error
11370        );
11371
11372        // Verify timelock is stored
11373        let acct = state.get_account(&contract_addr).unwrap().unwrap();
11374        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
11375        assert_eq!(ca.upgrade_timelock_epochs, Some(3));
11376        assert!(ca.pending_upgrade.is_none());
11377
11378        // Submit upgrade — should be staged, not applied immediately
11379        let new_code = valid_wasm_code(0x01);
11380        let result = submit_contract_ix(
11381            &processor,
11382            &alice_kp,
11383            vec![alice, contract_addr],
11384            crate::ContractInstruction::Upgrade {
11385                code: new_code.clone(),
11386            },
11387            genesis_hash,
11388            &validator,
11389        );
11390        assert!(
11391            result.success,
11392            "Timelocked upgrade should succeed (staged): {:?}",
11393            result.error
11394        );
11395
11396        // Verify pending upgrade exists but code not applied yet
11397        let acct = state.get_account(&contract_addr).unwrap().unwrap();
11398        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
11399        assert!(ca.pending_upgrade.is_some(), "Should have pending upgrade");
11400        assert_eq!(ca.version, 1, "Version should NOT have bumped yet");
11401        let pending = ca.pending_upgrade.unwrap();
11402        assert_eq!(pending.code, new_code);
11403        assert_eq!(pending.execute_after_epoch, pending.submitted_epoch + 3);
11404    }
11405
11406    #[test]
11407    fn test_code_hash_deploy_block_rejects_contract_upgrade_stage_and_execute() {
11408        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11409        let validator = Pubkey([42u8; 32]);
11410
11411        let contract_addr = deploy_test_contract(
11412            &processor,
11413            &state,
11414            &alice_kp,
11415            alice,
11416            genesis_hash,
11417            &validator,
11418        );
11419
11420        let banned_immediate_code = valid_wasm_code(0x42);
11421        let banned_immediate_hash = Hash::hash(&banned_immediate_code);
11422        let immediate_restriction_id = put_active_processor_test_restriction(
11423            &state,
11424            RestrictionTarget::CodeHash(banned_immediate_hash),
11425            RestrictionMode::DeployBlocked,
11426        );
11427        let result = submit_contract_ix(
11428            &processor,
11429            &alice_kp,
11430            vec![alice, contract_addr],
11431            crate::ContractInstruction::Upgrade {
11432                code: banned_immediate_code,
11433            },
11434            genesis_hash,
11435            &validator,
11436        );
11437        assert!(
11438            !result.success,
11439            "Immediate upgrade to a banned code hash must fail"
11440        );
11441        let error = result.error.as_deref().unwrap_or_default();
11442        assert!(error.contains("ContractUpgrade rejected"));
11443        assert!(error.contains("DeployBlocked"));
11444        assert!(error.contains(&immediate_restriction_id.to_string()));
11445        let contract = load_contract_account_for_test(&state, contract_addr);
11446        assert_eq!(contract.version, 1);
11447        assert!(contract.pending_upgrade.is_none());
11448        assert_ne!(contract.code_hash, banned_immediate_hash);
11449
11450        let result = submit_contract_ix(
11451            &processor,
11452            &alice_kp,
11453            vec![alice, contract_addr],
11454            crate::ContractInstruction::SetUpgradeTimelock { epochs: 1 },
11455            genesis_hash,
11456            &validator,
11457        );
11458        assert!(
11459            result.success,
11460            "SetUpgradeTimelock should succeed: {:?}",
11461            result.error
11462        );
11463
11464        let pending_code = valid_wasm_code(0x43);
11465        let pending_hash = Hash::hash(&pending_code);
11466        let result = submit_contract_ix(
11467            &processor,
11468            &alice_kp,
11469            vec![alice, contract_addr],
11470            crate::ContractInstruction::Upgrade { code: pending_code },
11471            genesis_hash,
11472            &validator,
11473        );
11474        assert!(result.success, "Upgrade should stage: {:?}", result.error);
11475        let pending_restriction_id = put_active_processor_test_restriction(
11476            &state,
11477            RestrictionTarget::CodeHash(pending_hash),
11478            RestrictionMode::DeployBlocked,
11479        );
11480        let execute_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH * 2);
11481
11482        let result = submit_contract_ix(
11483            &processor,
11484            &alice_kp,
11485            vec![alice, contract_addr],
11486            crate::ContractInstruction::ExecuteUpgrade,
11487            execute_blockhash,
11488            &validator,
11489        );
11490        assert!(
11491            !result.success,
11492            "Executing a pending upgrade to a banned code hash must fail"
11493        );
11494        let error = result.error.as_deref().unwrap_or_default();
11495        assert!(error.contains("ExecuteContractUpgrade rejected"));
11496        assert!(error.contains("DeployBlocked"));
11497        assert!(error.contains(&pending_restriction_id.to_string()));
11498        let contract = load_contract_account_for_test(&state, contract_addr);
11499        assert_eq!(contract.version, 1);
11500        assert_eq!(
11501            contract
11502                .pending_upgrade
11503                .as_ref()
11504                .map(|pending| pending.code_hash),
11505            Some(pending_hash)
11506        );
11507        assert_ne!(contract.code_hash, pending_hash);
11508    }
11509
11510    #[test]
11511    fn test_upgrade_without_timelock_is_instant() {
11512        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11513        let validator = Pubkey([42u8; 32]);
11514
11515        let contract_addr = deploy_test_contract(
11516            &processor,
11517            &state,
11518            &alice_kp,
11519            alice,
11520            genesis_hash,
11521            &validator,
11522        );
11523
11524        // No timelock set — upgrade should be instant
11525        let new_code = valid_wasm_code(0x02);
11526        let result = submit_contract_ix(
11527            &processor,
11528            &alice_kp,
11529            vec![alice, contract_addr],
11530            crate::ContractInstruction::Upgrade {
11531                code: new_code.clone(),
11532            },
11533            genesis_hash,
11534            &validator,
11535        );
11536        assert!(
11537            result.success,
11538            "Instant upgrade should succeed: {:?}",
11539            result.error
11540        );
11541
11542        let acct = state.get_account(&contract_addr).unwrap().unwrap();
11543        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
11544        assert_eq!(ca.version, 2, "Version should be bumped immediately");
11545        assert!(ca.pending_upgrade.is_none());
11546        assert_eq!(ca.code, new_code);
11547    }
11548
11549    #[test]
11550    fn test_contract_upgrade_uses_split_upgrade_proposer_when_owner_is_governed() {
11551        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11552        let validator = Pubkey([42u8; 32]);
11553        let bob_kp = Keypair::generate();
11554        let bob = bob_kp.pubkey();
11555        let gov_kp = Keypair::generate();
11556        let gov = gov_kp.pubkey();
11557
11558        let fund = Account::licn_to_spores(1_000);
11559        state
11560            .put_account(&alice, &Account::new(fund, alice))
11561            .unwrap();
11562        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
11563        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
11564        state.set_last_slot(0).unwrap();
11565        state.set_governance_authority(&gov).unwrap();
11566        state
11567            .set_governed_wallet_config(
11568                &gov,
11569                &crate::multisig::GovernedWalletConfig::new(
11570                    2,
11571                    vec![alice, bob, gov],
11572                    "community_treasury",
11573                )
11574                .with_timelock(1),
11575            )
11576            .unwrap();
11577        let upgrade_authority =
11578            configure_upgrade_proposer_for_test(&state, gov, 2, vec![alice, bob]);
11579
11580        let contract_addr =
11581            deploy_test_contract(&processor, &state, &gov_kp, gov, genesis_hash, &validator);
11582
11583        let direct = submit_contract_ix(
11584            &processor,
11585            &gov_kp,
11586            vec![gov, contract_addr],
11587            crate::ContractInstruction::Upgrade {
11588                code: valid_wasm_code(0x22),
11589            },
11590            genesis_hash,
11591            &validator,
11592        );
11593        assert!(!direct.success);
11594        assert!(direct
11595            .error
11596            .as_deref()
11597            .unwrap_or("")
11598            .contains("proposal flow"));
11599
11600        let code = valid_wasm_code(0x23);
11601        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_CONTRACT_UPGRADE];
11602        propose_data.extend_from_slice(&(code.len() as u32).to_le_bytes());
11603        propose_data.extend_from_slice(&code);
11604        let propose_ix = Instruction {
11605            program_id: SYSTEM_PROGRAM_ID,
11606            accounts: vec![alice, upgrade_authority, contract_addr],
11607            data: propose_data,
11608        };
11609        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
11610        let result = processor.process_transaction(&propose_tx, &validator);
11611        assert!(
11612            result.success,
11613            "Proposal should succeed: {:?}",
11614            result.error
11615        );
11616
11617        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
11618        assert_eq!(proposal.authority, gov);
11619        assert_eq!(proposal.approval_authority, Some(upgrade_authority));
11620        assert_eq!(proposal.execute_after_epoch, 1);
11621
11622        let mut approve_data = vec![35u8];
11623        approve_data.extend_from_slice(&1u64.to_le_bytes());
11624        let approve_ix = Instruction {
11625            program_id: SYSTEM_PROGRAM_ID,
11626            accounts: vec![bob],
11627            data: approve_data,
11628        };
11629        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
11630        let result = processor.process_transaction(&approve_tx, &validator);
11631        assert!(
11632            result.success,
11633            "Approval should succeed: {:?}",
11634            result.error
11635        );
11636
11637        let mut execute_data = vec![36u8];
11638        execute_data.extend_from_slice(&1u64.to_le_bytes());
11639        let execute_ix = Instruction {
11640            program_id: SYSTEM_PROGRAM_ID,
11641            accounts: vec![alice],
11642            data: execute_data.clone(),
11643        };
11644        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
11645        let result = processor.process_transaction(&execute_tx, &validator);
11646        assert!(!result.success);
11647        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
11648
11649        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
11650
11651        let execute_ix = Instruction {
11652            program_id: SYSTEM_PROGRAM_ID,
11653            accounts: vec![bob],
11654            data: execute_data,
11655        };
11656        let execute_tx = make_signed_tx(&bob_kp, execute_ix, fresh_blockhash);
11657        let result = processor.process_transaction(&execute_tx, &validator);
11658        assert!(
11659            result.success,
11660            "Execution should succeed: {:?}",
11661            result.error
11662        );
11663
11664        let acct = state.get_account(&contract_addr).unwrap().unwrap();
11665        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
11666        assert_eq!(ca.version, 2);
11667        assert!(ca.pending_upgrade.is_none());
11668        assert_eq!(ca.code, code);
11669    }
11670
11671    #[test]
11672    fn test_contract_upgrade_rejects_general_governance_authority_when_split_is_configured() {
11673        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11674        let validator = Pubkey([42u8; 32]);
11675        let bob = Pubkey([0x34; 32]);
11676        let gov_kp = Keypair::generate();
11677        let gov = gov_kp.pubkey();
11678
11679        let fund = Account::licn_to_spores(1_000);
11680        state
11681            .put_account(&alice, &Account::new(fund, alice))
11682            .unwrap();
11683        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
11684        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
11685        state.set_governance_authority(&gov).unwrap();
11686        state
11687            .set_governed_wallet_config(
11688                &gov,
11689                &crate::multisig::GovernedWalletConfig::new(
11690                    2,
11691                    vec![alice, bob, gov],
11692                    "community_treasury",
11693                )
11694                .with_timelock(1),
11695            )
11696            .unwrap();
11697        configure_upgrade_proposer_for_test(&state, gov, 2, vec![alice, bob]);
11698
11699        let contract_addr =
11700            deploy_test_contract(&processor, &state, &gov_kp, gov, genesis_hash, &validator);
11701        let code = valid_wasm_code(0x24);
11702        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_CONTRACT_UPGRADE];
11703        propose_data.extend_from_slice(&(code.len() as u32).to_le_bytes());
11704        propose_data.extend_from_slice(&code);
11705        let propose_ix = Instruction {
11706            program_id: SYSTEM_PROGRAM_ID,
11707            accounts: vec![alice, gov, contract_addr],
11708            data: propose_data,
11709        };
11710        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
11711        let propose_result = processor.process_transaction(&propose_tx, &validator);
11712        assert!(!propose_result.success);
11713        assert!(propose_result.error.as_deref().unwrap_or("").contains(
11714            "Upgrade governance actions must use the upgrade proposer approval authority"
11715        ));
11716    }
11717
11718    #[test]
11719    fn test_contract_call_requires_governance_proposal_when_authority_is_governed() {
11720        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11721        let validator = Pubkey([42u8; 32]);
11722        let bob_kp = Keypair::generate();
11723        let bob = bob_kp.pubkey();
11724        let gov_kp = Keypair::generate();
11725        let gov = gov_kp.pubkey();
11726
11727        let fund = Account::licn_to_spores(1_000);
11728        state
11729            .put_account(&alice, &Account::new(fund, alice))
11730            .unwrap();
11731        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
11732        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
11733        state.set_last_slot(0).unwrap();
11734        state.set_governance_authority(&gov).unwrap();
11735        state
11736            .set_governed_wallet_config(
11737                &gov,
11738                &crate::multisig::GovernedWalletConfig::new(
11739                    2,
11740                    vec![alice, bob, gov],
11741                    "community_treasury",
11742                )
11743                .with_timelock(1),
11744            )
11745            .unwrap();
11746
11747        let contract_addr =
11748            install_test_contract_account(&state, alice, governance_test_contract_code());
11749        let call_args = b"pause".to_vec();
11750
11751        let direct = submit_contract_ix(
11752            &processor,
11753            &gov_kp,
11754            vec![gov, contract_addr],
11755            crate::ContractInstruction::Call {
11756                function: "record_call".to_string(),
11757                args: call_args.clone(),
11758                value: 0,
11759            },
11760            genesis_hash,
11761            &validator,
11762        );
11763        assert!(!direct.success);
11764        assert!(direct
11765            .error
11766            .as_deref()
11767            .unwrap_or("")
11768            .contains("proposal flow"));
11769
11770        let propose_ix = Instruction {
11771            program_id: SYSTEM_PROGRAM_ID,
11772            accounts: vec![alice, gov, contract_addr],
11773            data: make_governance_contract_call_data("record_call", &call_args, 0),
11774        };
11775        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
11776        let result = processor.process_transaction(&propose_tx, &validator);
11777        assert!(
11778            result.success,
11779            "Proposal should succeed: {:?}",
11780            result.error
11781        );
11782
11783        let mut approve_data = vec![35u8];
11784        approve_data.extend_from_slice(&1u64.to_le_bytes());
11785        let approve_ix = Instruction {
11786            program_id: SYSTEM_PROGRAM_ID,
11787            accounts: vec![bob],
11788            data: approve_data,
11789        };
11790        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
11791        let result = processor.process_transaction(&approve_tx, &validator);
11792        assert!(
11793            result.success,
11794            "Approval should succeed: {:?}",
11795            result.error
11796        );
11797
11798        let mut execute_data = vec![36u8];
11799        execute_data.extend_from_slice(&1u64.to_le_bytes());
11800        let execute_ix = Instruction {
11801            program_id: SYSTEM_PROGRAM_ID,
11802            accounts: vec![alice],
11803            data: execute_data.clone(),
11804        };
11805        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
11806        let result = processor.process_transaction(&execute_tx, &validator);
11807        assert!(!result.success);
11808        assert!(result.error.as_deref().unwrap_or("").contains("timelocked"));
11809
11810        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
11811
11812        let execute_ix = Instruction {
11813            program_id: SYSTEM_PROGRAM_ID,
11814            accounts: vec![bob],
11815            data: execute_data,
11816        };
11817        let execute_tx = make_signed_tx(&bob_kp, execute_ix, fresh_blockhash);
11818        let result = processor.process_transaction(&execute_tx, &validator);
11819        assert!(
11820            result.success,
11821            "Execution should succeed: {:?}",
11822            result.error
11823        );
11824
11825        assert_eq!(
11826            state
11827                .get_contract_storage(&contract_addr, b"last_caller")
11828                .unwrap()
11829                .unwrap(),
11830            gov.0.to_vec()
11831        );
11832        assert_eq!(
11833            state
11834                .get_contract_storage(&contract_addr, b"last_args")
11835                .unwrap()
11836                .unwrap(),
11837            call_args
11838        );
11839    }
11840
11841    #[test]
11842    fn test_generic_admin_and_authority_rotation_contract_calls_remain_on_cold_governance_root() {
11843        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11844        let validator = Pubkey([42u8; 32]);
11845        let bob = Pubkey([0x47; 32]);
11846        let gov = Pubkey([0x48; 32]);
11847
11848        let fund = Account::licn_to_spores(1_000);
11849        state
11850            .put_account(&alice, &Account::new(fund, alice))
11851            .unwrap();
11852        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
11853        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
11854        state.set_last_slot(0).unwrap();
11855        state.set_governance_authority(&gov).unwrap();
11856        state
11857            .set_governed_wallet_config(
11858                &gov,
11859                &crate::multisig::GovernedWalletConfig::new(
11860                    2,
11861                    vec![alice, bob, gov],
11862                    "community_treasury",
11863                )
11864                .with_timelock(5),
11865            )
11866            .unwrap();
11867        let treasury_authority =
11868            configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob]);
11869        configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
11870        configure_oracle_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
11871        configure_upgrade_proposer_for_test(&state, gov, 2, vec![alice, bob]);
11872        configure_upgrade_veto_guardian_for_test(&state, gov, 2, vec![alice, bob]);
11873        configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
11874
11875        let admin_contract =
11876            install_test_contract_account(&state, gov, governance_test_contract_code());
11877        register_contract_symbol_for_test(&state, gov, admin_contract, "DEXREWARDS");
11878
11879        let mut admin_args = vec![0u8; 65];
11880        admin_args[32] = 0x11;
11881        admin_args[64] = 1;
11882        let admin_ix = Instruction {
11883            program_id: SYSTEM_PROGRAM_ID,
11884            accounts: vec![alice, gov, admin_contract],
11885            data: make_governance_contract_call_data("set_authorized_caller", &admin_args, 0),
11886        };
11887        let admin_tx = make_signed_tx(&alice_kp, admin_ix, genesis_hash);
11888        let admin_result = processor.process_transaction(&admin_tx, &validator);
11889        assert!(
11890            admin_result.success,
11891            "Generic admin proposal should stay on cold governance root: {:?}",
11892            admin_result.error
11893        );
11894
11895        let admin_proposal = state.get_governance_proposal(1).unwrap().unwrap();
11896        assert_eq!(admin_proposal.authority, gov);
11897        assert_eq!(admin_proposal.approval_authority, None);
11898        assert_eq!(admin_proposal.execute_after_epoch, 5);
11899
11900        let rotation_contract =
11901            install_test_contract_account(&state, gov, governance_test_contract_code());
11902        register_contract_symbol_for_test(&state, gov, rotation_contract, "LUSD");
11903
11904        let mut rotation_args = vec![0u8; 64];
11905        rotation_args[32] = 0x22;
11906        let rotation_ix = Instruction {
11907            program_id: SYSTEM_PROGRAM_ID,
11908            accounts: vec![alice, gov, rotation_contract],
11909            data: make_governance_contract_call_data("transfer_admin", &rotation_args, 0),
11910        };
11911        let rotation_tx = make_signed_tx(&alice_kp, rotation_ix, genesis_hash);
11912        let rotation_result = processor.process_transaction(&rotation_tx, &validator);
11913        assert!(
11914            rotation_result.success,
11915            "Authority rotation proposal should stay on cold governance root: {:?}",
11916            rotation_result.error
11917        );
11918
11919        let rotation_proposal = state.get_governance_proposal(2).unwrap().unwrap();
11920        assert_eq!(rotation_proposal.authority, gov);
11921        assert_eq!(rotation_proposal.approval_authority, None);
11922        assert_eq!(rotation_proposal.execute_after_epoch, 5);
11923
11924        let minter_ix = Instruction {
11925            program_id: SYSTEM_PROGRAM_ID,
11926            accounts: vec![alice, treasury_authority, rotation_contract],
11927            data: make_governance_contract_call_data("set_minter", &rotation_args, 0),
11928        };
11929        let minter_tx = make_signed_tx(&alice_kp, minter_ix, genesis_hash);
11930        let minter_result = processor.process_transaction(&minter_tx, &validator);
11931        assert!(
11932            minter_result.success,
11933            "Wrapped-token minter rotation should use treasury executor approvals: {:?}",
11934            minter_result.error
11935        );
11936
11937        let minter_proposal = state.get_governance_proposal(3).unwrap().unwrap();
11938        assert_eq!(minter_proposal.authority, gov);
11939        assert_eq!(minter_proposal.approval_authority, Some(treasury_authority));
11940        assert_eq!(minter_proposal.execute_after_epoch, 1);
11941
11942        let attester_ix = Instruction {
11943            program_id: SYSTEM_PROGRAM_ID,
11944            accounts: vec![
11945                alice,
11946                state
11947                    .get_oracle_committee_admin_authority()
11948                    .unwrap()
11949                    .unwrap(),
11950                rotation_contract,
11951            ],
11952            data: make_governance_contract_call_data("set_attester", &rotation_args, 0),
11953        };
11954        let attester_tx = make_signed_tx(&alice_kp, attester_ix, genesis_hash);
11955        let attester_result = processor.process_transaction(&attester_tx, &validator);
11956        assert!(
11957            attester_result.success,
11958            "Wrapped-token attester rotation should use oracle committee approvals: {:?}",
11959            attester_result.error
11960        );
11961
11962        let attester_proposal = state.get_governance_proposal(4).unwrap().unwrap();
11963        assert_eq!(attester_proposal.authority, gov);
11964        assert_eq!(
11965            attester_proposal.approval_authority,
11966            state.get_oracle_committee_admin_authority().unwrap()
11967        );
11968        assert_eq!(attester_proposal.execute_after_epoch, 1);
11969
11970        let wrong_role_ix = Instruction {
11971            program_id: SYSTEM_PROGRAM_ID,
11972            accounts: vec![alice, treasury_authority, rotation_contract],
11973            data: make_governance_contract_call_data("transfer_admin", &rotation_args, 0),
11974        };
11975        let wrong_role_tx = make_signed_tx(&alice_kp, wrong_role_ix, genesis_hash);
11976        let wrong_role_result = processor.process_transaction(&wrong_role_tx, &validator);
11977        assert!(!wrong_role_result.success);
11978        assert!(wrong_role_result
11979            .error
11980            .as_deref()
11981            .unwrap_or("")
11982            .contains("Governance action authority account mismatch"));
11983    }
11984
11985    #[test]
11986    fn test_allowlisted_emergency_pause_contract_call_uses_incident_guardian_without_timelock() {
11987        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
11988        let validator = Pubkey([42u8; 32]);
11989        let bob_kp = Keypair::generate();
11990        let bob = bob_kp.pubkey();
11991        let gov = Pubkey([0xB1; 32]);
11992
11993        let fund = Account::licn_to_spores(1_000);
11994        state
11995            .put_account(&alice, &Account::new(fund, alice))
11996            .unwrap();
11997        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
11998        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
11999        state.set_governance_authority(&gov).unwrap();
12000        state
12001            .set_governed_wallet_config(
12002                &gov,
12003                &crate::multisig::GovernedWalletConfig::new(
12004                    2,
12005                    vec![alice, bob, gov],
12006                    "community_treasury",
12007                )
12008                .with_timelock(5),
12009            )
12010            .unwrap();
12011        let guardian_authority =
12012            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
12013
12014        let contract_addr =
12015            install_test_contract_account(&state, gov, governance_test_contract_code());
12016        register_contract_symbol_for_test(&state, gov, contract_addr, "BRIDGE");
12017
12018        let propose_ix = Instruction {
12019            program_id: SYSTEM_PROGRAM_ID,
12020            accounts: vec![alice, guardian_authority, contract_addr],
12021            data: make_governance_contract_call_data("mb_pause", &[], 0),
12022        };
12023        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12024        let propose_result = processor.process_transaction(&propose_tx, &validator);
12025        assert!(
12026            propose_result.success,
12027            "Proposal should succeed: {:?}",
12028            propose_result.error
12029        );
12030
12031        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12032        assert_eq!(proposal.authority, gov);
12033        assert_eq!(proposal.approval_authority, Some(guardian_authority));
12034        assert_eq!(proposal.execute_after_epoch, 0);
12035        assert!(!proposal.executed);
12036
12037        let mut approve_data = vec![35u8];
12038        approve_data.extend_from_slice(&1u64.to_le_bytes());
12039        let approve_ix = Instruction {
12040            program_id: SYSTEM_PROGRAM_ID,
12041            accounts: vec![bob],
12042            data: approve_data,
12043        };
12044        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12045        let approve_result = processor.process_transaction(&approve_tx, &validator);
12046        assert!(
12047            approve_result.success,
12048            "Approval should execute the pause immediately: {:?}",
12049            approve_result.error
12050        );
12051
12052        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12053        assert!(proposal.executed);
12054        assert_eq!(
12055            state
12056                .get_contract_storage(&contract_addr, b"last_caller")
12057                .unwrap()
12058                .unwrap(),
12059            gov.0.to_vec()
12060        );
12061    }
12062
12063    #[test]
12064    fn test_allowlisted_emergency_pause_contract_call_stays_timelocked_on_governance_authority() {
12065        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12066        let validator = Pubkey([42u8; 32]);
12067        let bob_kp = Keypair::generate();
12068        let bob = bob_kp.pubkey();
12069        let gov = Pubkey([0xB4; 32]);
12070
12071        let fund = Account::licn_to_spores(1_000);
12072        state
12073            .put_account(&alice, &Account::new(fund, alice))
12074            .unwrap();
12075        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12076        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12077        state.set_governance_authority(&gov).unwrap();
12078        state
12079            .set_governed_wallet_config(
12080                &gov,
12081                &crate::multisig::GovernedWalletConfig::new(
12082                    2,
12083                    vec![alice, bob, gov],
12084                    "community_treasury",
12085                )
12086                .with_timelock(5),
12087            )
12088            .unwrap();
12089        configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
12090
12091        let contract_addr =
12092            install_test_contract_account(&state, gov, governance_test_contract_code());
12093        register_contract_symbol_for_test(&state, gov, contract_addr, "BRIDGE");
12094
12095        let propose_ix = Instruction {
12096            program_id: SYSTEM_PROGRAM_ID,
12097            accounts: vec![alice, gov, contract_addr],
12098            data: make_governance_contract_call_data("mb_pause", &[], 0),
12099        };
12100        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12101        assert!(
12102            processor
12103                .process_transaction(&propose_tx, &validator)
12104                .success
12105        );
12106
12107        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12108        assert_eq!(proposal.authority, gov);
12109        assert_eq!(proposal.approval_authority, None);
12110        assert_eq!(proposal.execute_after_epoch, 5);
12111        assert!(!proposal.executed);
12112
12113        let mut approve_data = vec![35u8];
12114        approve_data.extend_from_slice(&1u64.to_le_bytes());
12115        let approve_ix = Instruction {
12116            program_id: SYSTEM_PROGRAM_ID,
12117            accounts: vec![bob],
12118            data: approve_data,
12119        };
12120        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12121        let approve_result = processor.process_transaction(&approve_tx, &validator);
12122        assert!(
12123            approve_result.success,
12124            "Approval should keep the proposal pending behind the governance timelock: {:?}",
12125            approve_result.error
12126        );
12127
12128        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12129        assert!(!proposal.executed);
12130    }
12131
12132    #[test]
12133    fn test_non_allowlisted_emergency_pause_contract_call_stays_timelocked() {
12134        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12135        let validator = Pubkey([42u8; 32]);
12136        let bob_kp = Keypair::generate();
12137        let bob = bob_kp.pubkey();
12138        let gov = Pubkey([0xB2; 32]);
12139
12140        let fund = Account::licn_to_spores(1_000);
12141        state
12142            .put_account(&alice, &Account::new(fund, alice))
12143            .unwrap();
12144        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12145        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12146        state.set_governance_authority(&gov).unwrap();
12147        state
12148            .set_governed_wallet_config(
12149                &gov,
12150                &crate::multisig::GovernedWalletConfig::new(
12151                    2,
12152                    vec![alice, bob],
12153                    "community_treasury",
12154                )
12155                .with_timelock(5),
12156            )
12157            .unwrap();
12158
12159        let contract_addr =
12160            install_test_contract_account(&state, gov, governance_test_contract_code());
12161        register_contract_symbol_for_test(&state, gov, contract_addr, "LUSD");
12162
12163        let propose_ix = Instruction {
12164            program_id: SYSTEM_PROGRAM_ID,
12165            accounts: vec![alice, gov, contract_addr],
12166            data: make_governance_contract_call_data("emergency_pause", &[], 0),
12167        };
12168        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12169        assert!(
12170            processor
12171                .process_transaction(&propose_tx, &validator)
12172                .success
12173        );
12174
12175        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12176        assert_eq!(proposal.execute_after_epoch, 5);
12177        assert!(!proposal.executed);
12178
12179        let mut approve_data = vec![35u8];
12180        approve_data.extend_from_slice(&1u64.to_le_bytes());
12181        let approve_ix = Instruction {
12182            program_id: SYSTEM_PROGRAM_ID,
12183            accounts: vec![bob],
12184            data: approve_data,
12185        };
12186        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12187        let approve_result = processor.process_transaction(&approve_tx, &validator);
12188        assert!(
12189            approve_result.success,
12190            "Approval should keep the proposal pending behind the timelock: {:?}",
12191            approve_result.error
12192        );
12193
12194        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12195        assert!(!proposal.executed);
12196    }
12197
12198    #[test]
12199    fn test_incident_guardian_rejects_non_allowlisted_contract_call() {
12200        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12201        let validator = Pubkey([42u8; 32]);
12202        let bob = Pubkey([0x35; 32]);
12203        let gov = Pubkey([0xB5; 32]);
12204
12205        let fund = Account::licn_to_spores(1_000);
12206        state
12207            .put_account(&alice, &Account::new(fund, alice))
12208            .unwrap();
12209        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12210        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12211        state.set_governance_authority(&gov).unwrap();
12212        state
12213            .set_governed_wallet_config(
12214                &gov,
12215                &crate::multisig::GovernedWalletConfig::new(
12216                    2,
12217                    vec![alice, bob, gov],
12218                    "community_treasury",
12219                )
12220                .with_timelock(5),
12221            )
12222            .unwrap();
12223        let guardian_authority =
12224            configure_incident_guardian_for_test(&state, gov, 2, vec![alice, bob]);
12225
12226        let contract_addr =
12227            install_test_contract_account(&state, gov, governance_test_contract_code());
12228        register_contract_symbol_for_test(&state, gov, contract_addr, "LUSD");
12229
12230        let propose_ix = Instruction {
12231            program_id: SYSTEM_PROGRAM_ID,
12232            accounts: vec![alice, guardian_authority, contract_addr],
12233            data: make_governance_contract_call_data("emergency_pause", &[], 0),
12234        };
12235        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12236        let propose_result = processor.process_transaction(&propose_tx, &validator);
12237        assert!(!propose_result.success);
12238        assert!(propose_result
12239            .error
12240            .as_deref()
12241            .unwrap_or("")
12242            .contains("Incident guardian authority may only submit allowlisted immediate risk-reduction proposals"));
12243    }
12244
12245    #[test]
12246    fn test_allowlisted_unpause_contract_call_remains_timelocked() {
12247        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12248        let validator = Pubkey([42u8; 32]);
12249        let bob_kp = Keypair::generate();
12250        let bob = bob_kp.pubkey();
12251        let gov = Pubkey([0xB3; 32]);
12252
12253        let fund = Account::licn_to_spores(1_000);
12254        state
12255            .put_account(&alice, &Account::new(fund, alice))
12256            .unwrap();
12257        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12258        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12259        state.set_governance_authority(&gov).unwrap();
12260        state
12261            .set_governed_wallet_config(
12262                &gov,
12263                &crate::multisig::GovernedWalletConfig::new(
12264                    2,
12265                    vec![alice, bob],
12266                    "community_treasury",
12267                )
12268                .with_timelock(5),
12269            )
12270            .unwrap();
12271
12272        let contract_addr =
12273            install_test_contract_account(&state, gov, governance_test_contract_code());
12274        register_contract_symbol_for_test(&state, gov, contract_addr, "BRIDGE");
12275
12276        let propose_ix = Instruction {
12277            program_id: SYSTEM_PROGRAM_ID,
12278            accounts: vec![alice, gov, contract_addr],
12279            data: make_governance_contract_call_data("mb_unpause", &[], 0),
12280        };
12281        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12282        assert!(
12283            processor
12284                .process_transaction(&propose_tx, &validator)
12285                .success
12286        );
12287
12288        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12289        assert_eq!(proposal.execute_after_epoch, 5);
12290        assert!(!proposal.executed);
12291
12292        let mut approve_data = vec![35u8];
12293        approve_data.extend_from_slice(&1u64.to_le_bytes());
12294        let approve_ix = Instruction {
12295            program_id: SYSTEM_PROGRAM_ID,
12296            accounts: vec![bob],
12297            data: approve_data,
12298        };
12299        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12300        let approve_result = processor.process_transaction(&approve_tx, &validator);
12301        assert!(
12302            approve_result.success,
12303            "Approval should leave unpause behind the timelock: {:?}",
12304            approve_result.error
12305        );
12306
12307        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12308        assert!(!proposal.executed);
12309    }
12310
12311    #[test]
12312    fn test_bridge_committee_admin_contract_call_uses_split_approval_authority_and_timelock() {
12313        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12314        let validator = Pubkey([42u8; 32]);
12315        let bob_kp = Keypair::generate();
12316        let bob = bob_kp.pubkey();
12317        let gov = Pubkey([0xB8; 32]);
12318
12319        let fund = Account::licn_to_spores(1_000);
12320        state
12321            .put_account(&alice, &Account::new(fund, alice))
12322            .unwrap();
12323        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12324        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12325        state.set_governance_authority(&gov).unwrap();
12326        state
12327            .set_governed_wallet_config(
12328                &gov,
12329                &crate::multisig::GovernedWalletConfig::new(
12330                    2,
12331                    vec![alice, bob, gov],
12332                    "community_treasury",
12333                )
12334                .with_timelock(5),
12335            )
12336            .unwrap();
12337        let bridge_authority =
12338            configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
12339
12340        let contract_addr =
12341            install_test_contract_account(&state, gov, governance_test_contract_code());
12342        register_contract_symbol_for_test(&state, gov, contract_addr, "BRIDGE");
12343
12344        let propose_ix = Instruction {
12345            program_id: SYSTEM_PROGRAM_ID,
12346            accounts: vec![alice, bridge_authority, contract_addr],
12347            data: make_governance_contract_call_data("set_required_confirmations", &[], 0),
12348        };
12349        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12350        let propose_result = processor.process_transaction(&propose_tx, &validator);
12351        assert!(
12352            propose_result.success,
12353            "Proposal should succeed: {:?}",
12354            propose_result.error
12355        );
12356
12357        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12358        assert_eq!(proposal.authority, gov);
12359        assert_eq!(proposal.approval_authority, Some(bridge_authority));
12360        assert_eq!(proposal.execute_after_epoch, 1);
12361        assert!(!proposal.executed);
12362
12363        let mut approve_data = vec![35u8];
12364        approve_data.extend_from_slice(&1u64.to_le_bytes());
12365        let approve_ix = Instruction {
12366            program_id: SYSTEM_PROGRAM_ID,
12367            accounts: vec![bob],
12368            data: approve_data,
12369        };
12370        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12371        let approve_result = processor.process_transaction(&approve_tx, &validator);
12372        assert!(
12373            approve_result.success,
12374            "Approval should leave the proposal behind the committee timelock: {:?}",
12375            approve_result.error
12376        );
12377
12378        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12379        assert!(!proposal.executed);
12380    }
12381
12382    #[test]
12383    fn test_bridge_committee_admin_contract_call_rejects_governance_authority_direct_path() {
12384        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12385        let validator = Pubkey([42u8; 32]);
12386        let bob = Pubkey([0x36; 32]);
12387        let gov = Pubkey([0xB9; 32]);
12388
12389        let fund = Account::licn_to_spores(1_000);
12390        state
12391            .put_account(&alice, &Account::new(fund, alice))
12392            .unwrap();
12393        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12394        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12395        state.set_governance_authority(&gov).unwrap();
12396        state
12397            .set_governed_wallet_config(
12398                &gov,
12399                &crate::multisig::GovernedWalletConfig::new(
12400                    2,
12401                    vec![alice, bob, gov],
12402                    "community_treasury",
12403                )
12404                .with_timelock(5),
12405            )
12406            .unwrap();
12407        configure_bridge_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
12408
12409        let contract_addr =
12410            install_test_contract_account(&state, gov, governance_test_contract_code());
12411        register_contract_symbol_for_test(&state, gov, contract_addr, "BRIDGE");
12412
12413        let propose_ix = Instruction {
12414            program_id: SYSTEM_PROGRAM_ID,
12415            accounts: vec![alice, gov, contract_addr],
12416            data: make_governance_contract_call_data("set_request_timeout", &[], 0),
12417        };
12418        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12419        let propose_result = processor.process_transaction(&propose_tx, &validator);
12420        assert!(!propose_result.success);
12421        assert!(propose_result.error.as_deref().unwrap_or("").contains(
12422            "Bridge governance actions must use the bridge committee admin approval authority"
12423        ));
12424    }
12425
12426    #[test]
12427    fn test_oracle_committee_admin_contract_call_uses_split_approval_authority_and_timelock() {
12428        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12429        let validator = Pubkey([42u8; 32]);
12430        let bob_kp = Keypair::generate();
12431        let bob = bob_kp.pubkey();
12432        let gov = Pubkey([0xBA; 32]);
12433
12434        let fund = Account::licn_to_spores(1_000);
12435        state
12436            .put_account(&alice, &Account::new(fund, alice))
12437            .unwrap();
12438        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12439        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12440        state.set_governance_authority(&gov).unwrap();
12441        state
12442            .set_governed_wallet_config(
12443                &gov,
12444                &crate::multisig::GovernedWalletConfig::new(
12445                    2,
12446                    vec![alice, bob, gov],
12447                    "community_treasury",
12448                )
12449                .with_timelock(5),
12450            )
12451            .unwrap();
12452        let oracle_authority =
12453            configure_oracle_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
12454
12455        let contract_addr =
12456            install_test_contract_account(&state, gov, governance_test_contract_code());
12457        register_contract_symbol_for_test(&state, gov, contract_addr, "ORACLE");
12458
12459        let propose_ix = Instruction {
12460            program_id: SYSTEM_PROGRAM_ID,
12461            accounts: vec![alice, oracle_authority, contract_addr],
12462            data: make_governance_contract_call_data("set_authorized_attester", &[], 0),
12463        };
12464        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12465        let propose_result = processor.process_transaction(&propose_tx, &validator);
12466        assert!(
12467            propose_result.success,
12468            "Proposal should succeed: {:?}",
12469            propose_result.error
12470        );
12471
12472        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12473        assert_eq!(proposal.authority, gov);
12474        assert_eq!(proposal.approval_authority, Some(oracle_authority));
12475        assert_eq!(proposal.execute_after_epoch, 1);
12476        assert!(!proposal.executed);
12477
12478        let mut approve_data = vec![35u8];
12479        approve_data.extend_from_slice(&1u64.to_le_bytes());
12480        let approve_ix = Instruction {
12481            program_id: SYSTEM_PROGRAM_ID,
12482            accounts: vec![bob],
12483            data: approve_data,
12484        };
12485        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12486        let approve_result = processor.process_transaction(&approve_tx, &validator);
12487        assert!(
12488            approve_result.success,
12489            "Approval should leave the proposal behind the committee timelock: {:?}",
12490            approve_result.error
12491        );
12492
12493        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12494        assert!(!proposal.executed);
12495    }
12496
12497    #[test]
12498    fn test_upgrade_governance_actions_use_split_role_policies() {
12499        let (processor, _state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12500        let contract = Pubkey([0xBD; 32]);
12501
12502        assert!(
12503            processor.governance_action_requires_upgrade_proposer_policy(
12504                &GovernanceAction::ContractUpgrade {
12505                    contract,
12506                    code: vec![1, 2, 3],
12507                }
12508            )
12509        );
12510        assert!(
12511            processor.governance_action_requires_upgrade_proposer_policy(
12512                &GovernanceAction::SetContractUpgradeTimelock {
12513                    contract,
12514                    epochs: 2,
12515                }
12516            )
12517        );
12518        assert!(
12519            processor.governance_action_requires_upgrade_proposer_policy(
12520                &GovernanceAction::ExecuteContractUpgrade { contract }
12521            )
12522        );
12523        assert!(
12524            !processor.governance_action_requires_upgrade_proposer_policy(
12525                &GovernanceAction::VetoContractUpgrade { contract }
12526            )
12527        );
12528        assert!(
12529            processor.governance_action_requires_upgrade_veto_guardian_policy(
12530                &GovernanceAction::VetoContractUpgrade { contract }
12531            )
12532        );
12533    }
12534
12535    #[test]
12536    fn test_treasury_executor_policy_covers_protocol_outflow_contract_calls() {
12537        let (processor, state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12538        let owner = Pubkey([0xC4; 32]);
12539        let margin_contract = Pubkey([0xC5; 32]);
12540        let lend_contract = Pubkey([0xC6; 32]);
12541        let vault_contract = Pubkey([0xC7; 32]);
12542        let pump_contract = Pubkey([0xC8; 32]);
12543        let amm_contract = Pubkey([0xC9; 32]);
12544        let generic_contract = Pubkey([0xCA; 32]);
12545
12546        register_contract_symbol_for_test(&state, owner, margin_contract, "DEXMARGIN");
12547        register_contract_symbol_for_test(&state, owner, lend_contract, "LEND");
12548        register_contract_symbol_for_test(&state, owner, vault_contract, "SPOREVAULT");
12549        register_contract_symbol_for_test(&state, owner, pump_contract, "SPOREPUMP");
12550        register_contract_symbol_for_test(&state, owner, amm_contract, "DEXAMM");
12551        register_contract_symbol_for_test(&state, owner, generic_contract, "GENERIC");
12552
12553        let mut insurance_args = vec![0u8; 73];
12554        insurance_args[0] = 9u8;
12555        insurance_args[1] = 0x44;
12556        insurance_args[33..41].copy_from_slice(&500_000u64.to_le_bytes());
12557        insurance_args[41] = 0x99;
12558
12559        let mut amm_outflow_args = vec![0u8; 41];
12560        amm_outflow_args[0] = 21u8;
12561        amm_outflow_args[1] = 0x55;
12562        amm_outflow_args[33..41].copy_from_slice(&7u64.to_le_bytes());
12563
12564        let mut amm_admin_args = vec![0u8; 65];
12565        amm_admin_args[0] = 20u8;
12566        amm_admin_args[1] = 0x11;
12567        amm_admin_args[33] = 0x22;
12568
12569        let mut margin_admin_args = vec![0u8; 49];
12570        margin_admin_args[0] = 7u8;
12571
12572        assert!(processor
12573            .governance_action_requires_treasury_executor_policy(
12574                &GovernanceAction::TreasuryTransfer {
12575                    recipient: Pubkey([0xCA; 32]),
12576                    amount: 1,
12577                }
12578            )
12579            .unwrap());
12580        assert!(processor
12581            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12582                contract: margin_contract,
12583                function: "call".to_string(),
12584                args: insurance_args,
12585                value: 0,
12586            })
12587            .unwrap());
12588        assert!(processor
12589            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12590                contract: lend_contract,
12591                function: "withdraw_reserves".to_string(),
12592                args: vec![0u8; 8],
12593                value: 0,
12594            })
12595            .unwrap());
12596        assert!(processor
12597            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12598                contract: vault_contract,
12599                function: "withdraw_protocol_fees".to_string(),
12600                args: vec![],
12601                value: 0,
12602            })
12603            .unwrap());
12604        assert!(processor
12605            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12606                contract: amm_contract,
12607                function: "call".to_string(),
12608                args: amm_outflow_args,
12609                value: 0,
12610            })
12611            .unwrap());
12612        assert!(processor
12613            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12614                contract: pump_contract,
12615                function: "withdraw_fees".to_string(),
12616                args: 500_000u64.to_le_bytes().to_vec(),
12617                value: 0,
12618            })
12619            .unwrap());
12620        assert!(!processor
12621            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12622                contract: margin_contract,
12623                function: "call".to_string(),
12624                args: margin_admin_args,
12625                value: 0,
12626            })
12627            .unwrap());
12628        assert!(!processor
12629            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12630                contract: amm_contract,
12631                function: "call".to_string(),
12632                args: amm_admin_args,
12633                value: 0,
12634            })
12635            .unwrap());
12636        assert!(!processor
12637            .governance_action_requires_treasury_executor_policy(&GovernanceAction::ContractCall {
12638                contract: generic_contract,
12639                function: "withdraw_fees".to_string(),
12640                args: vec![],
12641                value: 0,
12642            })
12643            .unwrap());
12644    }
12645
12646    #[test]
12647    fn test_restriction_governance_actions_do_not_match_legacy_split_role_policies() {
12648        let (processor, _state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12649        let actions = vec![
12650            GovernanceAction::Restrict {
12651                target: RestrictionTarget::Account(Pubkey([0xD0; 32])),
12652                mode: RestrictionMode::OutgoingOnly,
12653                reason: RestrictionReason::TestnetDrill,
12654                evidence_hash: None,
12655                evidence_uri_hash: None,
12656                expires_at_slot: Some(100),
12657            },
12658            GovernanceAction::LiftRestriction {
12659                restriction_id: 1,
12660                reason: RestrictionLiftReason::IncidentResolved,
12661            },
12662            GovernanceAction::ExtendRestriction {
12663                restriction_id: 1,
12664                new_expires_at_slot: Some(200),
12665                evidence_hash: None,
12666            },
12667        ];
12668
12669        for action in actions {
12670            assert!(
12671                !processor
12672                    .governance_action_requires_treasury_executor_policy(&action)
12673                    .unwrap(),
12674                "{:?} must not use treasury executor routing",
12675                action
12676            );
12677            assert!(
12678                !processor.governance_action_requires_upgrade_proposer_policy(&action),
12679                "{:?} must not use upgrade proposer routing",
12680                action
12681            );
12682            assert!(
12683                !processor.governance_action_requires_upgrade_veto_guardian_policy(&action),
12684                "{:?} must not use upgrade veto guardian routing",
12685                action
12686            );
12687        }
12688    }
12689
12690    #[test]
12691    fn test_veto_upgrade_governance_action_uses_split_veto_guardian_authority() {
12692        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12693        let validator = Pubkey([42u8; 32]);
12694        let bob_kp = Keypair::generate();
12695        let bob = bob_kp.pubkey();
12696        let gov = Pubkey([0xBE; 32]);
12697
12698        let fund = Account::licn_to_spores(1_000);
12699        state
12700            .put_account(&alice, &Account::new(fund, alice))
12701            .unwrap();
12702        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12703        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12704        state.set_last_slot(0).unwrap();
12705        state.set_governance_authority(&gov).unwrap();
12706        state
12707            .set_governed_wallet_config(
12708                &gov,
12709                &crate::multisig::GovernedWalletConfig::new(
12710                    2,
12711                    vec![alice, bob, gov],
12712                    "community_treasury",
12713                )
12714                .with_timelock(1),
12715            )
12716            .unwrap();
12717        let veto_authority =
12718            configure_upgrade_veto_guardian_for_test(&state, gov, 2, vec![alice, bob]);
12719
12720        let contract_addr = deploy_test_contract(
12721            &processor,
12722            &state,
12723            &alice_kp,
12724            alice,
12725            genesis_hash,
12726            &validator,
12727        );
12728
12729        let result = submit_contract_ix(
12730            &processor,
12731            &alice_kp,
12732            vec![alice, contract_addr],
12733            crate::ContractInstruction::SetUpgradeTimelock { epochs: 2 },
12734            genesis_hash,
12735            &validator,
12736        );
12737        assert!(
12738            result.success,
12739            "Timelock should succeed: {:?}",
12740            result.error
12741        );
12742
12743        let result = submit_contract_ix(
12744            &processor,
12745            &alice_kp,
12746            vec![alice, contract_addr],
12747            crate::ContractInstruction::Upgrade {
12748                code: valid_wasm_code(0x31),
12749            },
12750            genesis_hash,
12751            &validator,
12752        );
12753        assert!(result.success, "Upgrade should stage: {:?}", result.error);
12754
12755        let propose_ix = Instruction {
12756            program_id: SYSTEM_PROGRAM_ID,
12757            accounts: vec![alice, veto_authority, contract_addr],
12758            data: vec![34u8, GOVERNANCE_ACTION_VETO_UPGRADE],
12759        };
12760        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12761        let propose_result = processor.process_transaction(&propose_tx, &validator);
12762        assert!(
12763            propose_result.success,
12764            "Proposal should succeed: {:?}",
12765            propose_result.error
12766        );
12767
12768        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12769        assert_eq!(proposal.authority, gov);
12770        assert_eq!(proposal.approval_authority, Some(veto_authority));
12771        assert_eq!(proposal.execute_after_epoch, 0);
12772        assert!(!proposal.executed);
12773
12774        let mut approve_data = vec![35u8];
12775        approve_data.extend_from_slice(&1u64.to_le_bytes());
12776        let approve_ix = Instruction {
12777            program_id: SYSTEM_PROGRAM_ID,
12778            accounts: vec![bob],
12779            data: approve_data,
12780        };
12781        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
12782        let approve_result = processor.process_transaction(&approve_tx, &validator);
12783        assert!(
12784            approve_result.success,
12785            "Approval should execute the veto immediately: {:?}",
12786            approve_result.error
12787        );
12788
12789        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
12790        assert!(proposal.executed);
12791
12792        let acct = state.get_account(&contract_addr).unwrap().unwrap();
12793        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
12794        assert!(ca.pending_upgrade.is_none());
12795        assert_eq!(ca.version, 1);
12796    }
12797
12798    #[test]
12799    fn test_veto_upgrade_rejects_general_governance_authority_when_split_is_configured() {
12800        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
12801        let validator = Pubkey([42u8; 32]);
12802        let bob = Pubkey([0x35; 32]);
12803        let gov = Pubkey([0xBF; 32]);
12804
12805        let fund = Account::licn_to_spores(1_000);
12806        state
12807            .put_account(&alice, &Account::new(fund, alice))
12808            .unwrap();
12809        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
12810        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
12811        state.set_governance_authority(&gov).unwrap();
12812        state
12813            .set_governed_wallet_config(
12814                &gov,
12815                &crate::multisig::GovernedWalletConfig::new(
12816                    2,
12817                    vec![alice, bob, gov],
12818                    "community_treasury",
12819                )
12820                .with_timelock(1),
12821            )
12822            .unwrap();
12823        configure_upgrade_veto_guardian_for_test(&state, gov, 2, vec![alice, bob]);
12824
12825        let contract_addr = deploy_test_contract(
12826            &processor,
12827            &state,
12828            &alice_kp,
12829            alice,
12830            genesis_hash,
12831            &validator,
12832        );
12833        let result = submit_contract_ix(
12834            &processor,
12835            &alice_kp,
12836            vec![alice, contract_addr],
12837            crate::ContractInstruction::SetUpgradeTimelock { epochs: 1 },
12838            genesis_hash,
12839            &validator,
12840        );
12841        assert!(result.success);
12842        let result = submit_contract_ix(
12843            &processor,
12844            &alice_kp,
12845            vec![alice, contract_addr],
12846            crate::ContractInstruction::Upgrade {
12847                code: valid_wasm_code(0x32),
12848            },
12849            genesis_hash,
12850            &validator,
12851        );
12852        assert!(result.success);
12853
12854        let propose_ix = Instruction {
12855            program_id: SYSTEM_PROGRAM_ID,
12856            accounts: vec![alice, gov, contract_addr],
12857            data: vec![34u8, GOVERNANCE_ACTION_VETO_UPGRADE],
12858        };
12859        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
12860        let propose_result = processor.process_transaction(&propose_tx, &validator);
12861        assert!(!propose_result.success);
12862        assert!(propose_result.error.as_deref().unwrap_or("").contains(
12863            "Upgrade veto governance actions must use the upgrade veto guardian approval authority"
12864        ));
12865    }
12866
12867    #[test]
12868    fn test_marketplace_pause_entries_are_allowlisted() {
12869        let (processor, state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12870        let owner = Pubkey([0xB6; 32]);
12871        let auction_contract = Pubkey([0xC1; 32]);
12872        let market_contract = Pubkey([0xC2; 32]);
12873
12874        register_contract_symbol_for_test(&state, owner, auction_contract, "AUCTION");
12875        register_contract_symbol_for_test(&state, owner, market_contract, "MARKET");
12876
12877        assert!(processor
12878            .governance_action_uses_immediate_risk_reduction_policy(
12879                &GovernanceAction::ContractCall {
12880                    contract: auction_contract,
12881                    function: "ma_pause".to_string(),
12882                    args: vec![],
12883                    value: 0,
12884                }
12885            )
12886            .unwrap());
12887        assert!(processor
12888            .governance_action_uses_immediate_risk_reduction_policy(
12889                &GovernanceAction::ContractCall {
12890                    contract: market_contract,
12891                    function: "mm_pause".to_string(),
12892                    args: vec![],
12893                    value: 0,
12894                }
12895            )
12896            .unwrap());
12897    }
12898
12899    #[test]
12900    fn test_additional_pause_safe_contract_entries_are_allowlisted() {
12901        let (processor, state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12902        let owner = Pubkey([0xB8; 32]);
12903        let compute_contract = Pubkey([0xD1; 32]);
12904        let predict_contract = Pubkey([0xD2; 32]);
12905        let pump_contract = Pubkey([0xD3; 32]);
12906
12907        register_contract_symbol_for_test(&state, owner, compute_contract, "COMPUTE");
12908        register_contract_symbol_for_test(&state, owner, predict_contract, "PREDICT");
12909        register_contract_symbol_for_test(&state, owner, pump_contract, "SPOREPUMP");
12910
12911        assert!(processor
12912            .governance_action_uses_immediate_risk_reduction_policy(
12913                &GovernanceAction::ContractCall {
12914                    contract: compute_contract,
12915                    function: "cm_pause".to_string(),
12916                    args: vec![],
12917                    value: 0,
12918                }
12919            )
12920            .unwrap());
12921        assert!(processor
12922            .governance_action_uses_immediate_risk_reduction_policy(
12923                &GovernanceAction::ContractCall {
12924                    contract: predict_contract,
12925                    function: "emergency_pause".to_string(),
12926                    args: vec![],
12927                    value: 0,
12928                }
12929            )
12930            .unwrap());
12931        assert!(processor
12932            .governance_action_uses_immediate_risk_reduction_policy(
12933                &GovernanceAction::ContractCall {
12934                    contract: pump_contract,
12935                    function: "pause".to_string(),
12936                    args: vec![],
12937                    value: 0,
12938                }
12939            )
12940            .unwrap());
12941    }
12942
12943    #[test]
12944    fn test_dex_pause_pair_entry_remains_allowlisted() {
12945        let (processor, state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12946        let owner = Pubkey([0xB7; 32]);
12947        let dex_contract = Pubkey([0xC3; 32]);
12948
12949        register_contract_symbol_for_test(&state, owner, dex_contract, "DEX");
12950
12951        assert!(processor
12952            .governance_action_uses_immediate_risk_reduction_policy(
12953                &GovernanceAction::ContractCall {
12954                    contract: dex_contract,
12955                    function: "pause_pair".to_string(),
12956                    args: vec![],
12957                    value: 0,
12958                }
12959            )
12960            .unwrap());
12961    }
12962
12963    #[test]
12964    fn test_margin_price_updates_use_oracle_committee_immediate_policy() {
12965        let (processor, state, _alice_kp, _alice, _treasury, _genesis_hash) = setup();
12966        let owner = Pubkey([0xB9; 32]);
12967        let margin_contract = Pubkey([0xD4; 32]);
12968
12969        register_contract_symbol_for_test(&state, owner, margin_contract, "DEXMARGIN");
12970
12971        for opcode in [1u8, 31u8] {
12972            let action = GovernanceAction::ContractCall {
12973                contract: margin_contract,
12974                function: "call".to_string(),
12975                args: vec![opcode],
12976                value: 0,
12977            };
12978            assert!(processor
12979                .governance_action_requires_oracle_committee_admin_policy(&action)
12980                .unwrap());
12981            assert!(processor
12982                .governance_action_uses_immediate_risk_reduction_policy(&action)
12983                .unwrap());
12984        }
12985    }
12986
12987    #[test]
12988    fn test_bridge_validator_change_requires_governance_proposal_when_authority_is_governed() {
12989        let mut call_args = vec![0u8; 64];
12990        call_args[32] = 0x55;
12991        assert_governed_committee_contract_call_requires_proposal(
12992            "add_bridge_validator",
12993            call_args,
12994        );
12995    }
12996
12997    #[test]
12998    fn test_bridge_threshold_change_requires_governance_proposal_when_authority_is_governed() {
12999        let mut call_args = vec![0u8; 40];
13000        call_args[0] = 0x11;
13001        call_args[32..40].copy_from_slice(&3u64.to_le_bytes());
13002        assert_governed_committee_contract_call_requires_proposal(
13003            "set_required_confirmations",
13004            call_args,
13005        );
13006    }
13007
13008    #[test]
13009    fn test_bridge_timeout_change_requires_governance_proposal_when_authority_is_governed() {
13010        let mut call_args = vec![0u8; 40];
13011        call_args[0] = 0x22;
13012        call_args[32..40].copy_from_slice(&1_000u64.to_le_bytes());
13013        assert_governed_committee_contract_call_requires_proposal("set_request_timeout", call_args);
13014    }
13015
13016    #[test]
13017    fn test_oracle_feeder_change_requires_governance_proposal_when_authority_is_governed() {
13018        let mut call_args = vec![0u8; 40];
13019        call_args[0] = 0x88;
13020        call_args[32..36].copy_from_slice(b"LICN");
13021        call_args[36..40].copy_from_slice(&4u32.to_le_bytes());
13022        assert_governed_committee_contract_call_requires_proposal("add_price_feeder", call_args);
13023    }
13024
13025    #[test]
13026    fn test_oracle_attester_change_requires_governance_proposal_when_authority_is_governed() {
13027        let mut call_args = vec![0u8; 36];
13028        call_args[0] = 0x77;
13029        call_args[32..36].copy_from_slice(&1u32.to_le_bytes());
13030        assert_governed_committee_contract_call_requires_proposal(
13031            "set_authorized_attester",
13032            call_args,
13033        );
13034    }
13035
13036    #[test]
13037    fn test_margin_price_update_contract_call_uses_oracle_committee_approval_authority_and_executes_immediately(
13038    ) {
13039        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13040        let validator = Pubkey([42u8; 32]);
13041        let bob_kp = Keypair::generate();
13042        let bob = bob_kp.pubkey();
13043        let gov_kp = Keypair::generate();
13044        let gov = gov_kp.pubkey();
13045
13046        let fund = Account::licn_to_spores(1_000);
13047        state
13048            .put_account(&alice, &Account::new(fund, alice))
13049            .unwrap();
13050        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13051        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13052        state.set_last_slot(0).unwrap();
13053        state.set_governance_authority(&gov).unwrap();
13054        state
13055            .set_governed_wallet_config(
13056                &gov,
13057                &crate::multisig::GovernedWalletConfig::new(
13058                    2,
13059                    vec![alice, bob, gov],
13060                    "community_treasury",
13061                )
13062                .with_timelock(5),
13063            )
13064            .unwrap();
13065        let oracle_authority =
13066            configure_oracle_committee_admin_for_test(&state, gov, 2, vec![alice, bob]);
13067
13068        let contract_addr =
13069            install_test_contract_account(&state, gov, governance_test_contract_code());
13070        register_contract_symbol_for_test(&state, gov, contract_addr, "DEXMARGIN");
13071
13072        let mut call_args = vec![0u8; 49];
13073        call_args[0] = 1u8;
13074        call_args[1] = 0x44;
13075        call_args[33..41].copy_from_slice(&7u64.to_le_bytes());
13076        call_args[41..49].copy_from_slice(&1_000_000u64.to_le_bytes());
13077
13078        let direct = submit_contract_ix(
13079            &processor,
13080            &gov_kp,
13081            vec![gov, contract_addr],
13082            crate::ContractInstruction::Call {
13083                function: "call".to_string(),
13084                args: call_args.clone(),
13085                value: 0,
13086            },
13087            genesis_hash,
13088            &validator,
13089        );
13090        assert!(!direct.success);
13091        assert!(direct
13092            .error
13093            .as_deref()
13094            .unwrap_or("")
13095            .contains("proposal flow"));
13096
13097        let propose_ix = Instruction {
13098            program_id: SYSTEM_PROGRAM_ID,
13099            accounts: vec![alice, oracle_authority, contract_addr],
13100            data: make_governance_contract_call_data("call", &call_args, 0),
13101        };
13102        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13103        let propose_result = processor.process_transaction(&propose_tx, &validator);
13104        assert!(
13105            propose_result.success,
13106            "Proposal should succeed: {:?}",
13107            propose_result.error
13108        );
13109
13110        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13111        assert_eq!(proposal.authority, gov);
13112        assert_eq!(proposal.approval_authority, Some(oracle_authority));
13113        assert_eq!(proposal.execute_after_epoch, 0);
13114        assert!(!proposal.executed);
13115
13116        let mut approve_data = vec![35u8];
13117        approve_data.extend_from_slice(&1u64.to_le_bytes());
13118        let approve_ix = Instruction {
13119            program_id: SYSTEM_PROGRAM_ID,
13120            accounts: vec![bob],
13121            data: approve_data,
13122        };
13123        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13124        let approve_result = processor.process_transaction(&approve_tx, &validator);
13125        assert!(
13126            approve_result.success,
13127            "Approval should execute immediately: {:?}",
13128            approve_result.error
13129        );
13130
13131        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13132        assert!(proposal.executed);
13133        assert_eq!(
13134            state
13135                .get_contract_storage(&contract_addr, b"last_caller")
13136                .unwrap()
13137                .unwrap(),
13138            gov.0.to_vec()
13139        );
13140        assert_eq!(
13141            state
13142                .get_contract_storage(&contract_addr, b"last_args")
13143                .unwrap()
13144                .unwrap(),
13145            call_args
13146        );
13147    }
13148
13149    #[test]
13150    fn test_margin_insurance_withdraw_contract_call_uses_treasury_executor_approval_authority_and_timelock(
13151    ) {
13152        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13153        let validator = Pubkey([42u8; 32]);
13154        let bob_kp = Keypair::generate();
13155        let bob = bob_kp.pubkey();
13156        let gov_kp = Keypair::generate();
13157        let gov = gov_kp.pubkey();
13158
13159        let fund = Account::licn_to_spores(1_000);
13160        state
13161            .put_account(&alice, &Account::new(fund, alice))
13162            .unwrap();
13163        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13164        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13165        state.set_last_slot(0).unwrap();
13166        state.set_governance_authority(&gov).unwrap();
13167        state
13168            .set_governed_wallet_config(
13169                &gov,
13170                &crate::multisig::GovernedWalletConfig::new(
13171                    2,
13172                    vec![alice, bob, gov],
13173                    "community_treasury",
13174                )
13175                .with_timelock(5),
13176            )
13177            .unwrap();
13178        let treasury_authority =
13179            configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob]);
13180
13181        let contract_addr =
13182            install_test_contract_account(&state, gov, governance_test_contract_code());
13183        register_contract_symbol_for_test(&state, gov, contract_addr, "DEXMARGIN");
13184
13185        let mut call_args = vec![0u8; 73];
13186        call_args[0] = 9u8;
13187        call_args[1] = 0x44;
13188        call_args[33..41].copy_from_slice(&500_000u64.to_le_bytes());
13189        call_args[41] = 0x99;
13190
13191        let direct = submit_contract_ix(
13192            &processor,
13193            &gov_kp,
13194            vec![gov, contract_addr],
13195            crate::ContractInstruction::Call {
13196                function: "call".to_string(),
13197                args: call_args.clone(),
13198                value: 0,
13199            },
13200            genesis_hash,
13201            &validator,
13202        );
13203        assert!(!direct.success);
13204        assert!(direct
13205            .error
13206            .as_deref()
13207            .unwrap_or("")
13208            .contains("proposal flow"));
13209
13210        let propose_ix = Instruction {
13211            program_id: SYSTEM_PROGRAM_ID,
13212            accounts: vec![alice, treasury_authority, contract_addr],
13213            data: make_governance_contract_call_data("call", &call_args, 0),
13214        };
13215        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13216        let propose_result = processor.process_transaction(&propose_tx, &validator);
13217        assert!(
13218            propose_result.success,
13219            "Proposal should succeed: {:?}",
13220            propose_result.error
13221        );
13222
13223        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13224        assert_eq!(proposal.authority, gov);
13225        assert_eq!(proposal.approval_authority, Some(treasury_authority));
13226        assert_eq!(proposal.execute_after_epoch, 1);
13227        assert!(!proposal.executed);
13228
13229        let mut approve_data = vec![35u8];
13230        approve_data.extend_from_slice(&1u64.to_le_bytes());
13231        let approve_ix = Instruction {
13232            program_id: SYSTEM_PROGRAM_ID,
13233            accounts: vec![bob],
13234            data: approve_data,
13235        };
13236        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13237        let approve_result = processor.process_transaction(&approve_tx, &validator);
13238        assert!(
13239            approve_result.success,
13240            "Approval should succeed: {:?}",
13241            approve_result.error
13242        );
13243
13244        let mut execute_data = vec![36u8];
13245        execute_data.extend_from_slice(&1u64.to_le_bytes());
13246        let execute_ix = Instruction {
13247            program_id: SYSTEM_PROGRAM_ID,
13248            accounts: vec![alice],
13249            data: execute_data.clone(),
13250        };
13251        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
13252        let execute_result = processor.process_transaction(&execute_tx, &validator);
13253        assert!(!execute_result.success);
13254        assert!(execute_result
13255            .error
13256            .as_deref()
13257            .unwrap_or("")
13258            .contains("timelocked"));
13259
13260        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
13261        let execute_ix = Instruction {
13262            program_id: SYSTEM_PROGRAM_ID,
13263            accounts: vec![bob],
13264            data: execute_data,
13265        };
13266        let execute_tx = make_signed_tx(&bob_kp, execute_ix, fresh_blockhash);
13267        let execute_result = processor.process_transaction(&execute_tx, &validator);
13268        assert!(
13269            execute_result.success,
13270            "Execution should succeed: {:?}",
13271            execute_result.error
13272        );
13273
13274        assert_eq!(
13275            state
13276                .get_contract_storage(&contract_addr, b"last_caller")
13277                .unwrap()
13278                .unwrap(),
13279            gov.0.to_vec()
13280        );
13281        assert_eq!(
13282            state
13283                .get_contract_storage(&contract_addr, b"last_args")
13284                .unwrap()
13285                .unwrap(),
13286            call_args
13287        );
13288    }
13289
13290    #[test]
13291    fn test_amm_protocol_fee_collection_contract_call_uses_treasury_executor_approval_authority_and_timelock(
13292    ) {
13293        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13294        let validator = Pubkey([42u8; 32]);
13295        let bob_kp = Keypair::generate();
13296        let bob = bob_kp.pubkey();
13297        let gov_kp = Keypair::generate();
13298        let gov = gov_kp.pubkey();
13299
13300        let fund = Account::licn_to_spores(1_000);
13301        state
13302            .put_account(&alice, &Account::new(fund, alice))
13303            .unwrap();
13304        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13305        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13306        state.set_last_slot(0).unwrap();
13307        state.set_governance_authority(&gov).unwrap();
13308        state
13309            .set_governed_wallet_config(
13310                &gov,
13311                &crate::multisig::GovernedWalletConfig::new(
13312                    2,
13313                    vec![alice, bob, gov],
13314                    "community_treasury",
13315                )
13316                .with_timelock(5),
13317            )
13318            .unwrap();
13319        let treasury_authority =
13320            configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob]);
13321
13322        let contract_addr =
13323            install_test_contract_account(&state, gov, governance_test_contract_code());
13324        register_contract_symbol_for_test(&state, gov, contract_addr, "DEXAMM");
13325
13326        let mut call_args = vec![0u8; 41];
13327        call_args[0] = 21u8;
13328        call_args[1] = 0x44;
13329        call_args[33..41].copy_from_slice(&7u64.to_le_bytes());
13330
13331        let direct = submit_contract_ix(
13332            &processor,
13333            &gov_kp,
13334            vec![gov, contract_addr],
13335            crate::ContractInstruction::Call {
13336                function: "call".to_string(),
13337                args: call_args.clone(),
13338                value: 0,
13339            },
13340            genesis_hash,
13341            &validator,
13342        );
13343        assert!(!direct.success);
13344        assert!(direct
13345            .error
13346            .as_deref()
13347            .unwrap_or("")
13348            .contains("proposal flow"));
13349
13350        let propose_ix = Instruction {
13351            program_id: SYSTEM_PROGRAM_ID,
13352            accounts: vec![alice, treasury_authority, contract_addr],
13353            data: make_governance_contract_call_data("call", &call_args, 0),
13354        };
13355        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13356        let propose_result = processor.process_transaction(&propose_tx, &validator);
13357        assert!(
13358            propose_result.success,
13359            "Proposal should succeed: {:?}",
13360            propose_result.error
13361        );
13362
13363        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13364        assert_eq!(proposal.authority, gov);
13365        assert_eq!(proposal.approval_authority, Some(treasury_authority));
13366        assert_eq!(proposal.execute_after_epoch, 1);
13367        assert!(!proposal.executed);
13368
13369        let mut approve_data = vec![35u8];
13370        approve_data.extend_from_slice(&1u64.to_le_bytes());
13371        let approve_ix = Instruction {
13372            program_id: SYSTEM_PROGRAM_ID,
13373            accounts: vec![bob],
13374            data: approve_data,
13375        };
13376        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13377        let approve_result = processor.process_transaction(&approve_tx, &validator);
13378        assert!(
13379            approve_result.success,
13380            "Approval should succeed: {:?}",
13381            approve_result.error
13382        );
13383
13384        let mut execute_data = vec![36u8];
13385        execute_data.extend_from_slice(&1u64.to_le_bytes());
13386        let execute_ix = Instruction {
13387            program_id: SYSTEM_PROGRAM_ID,
13388            accounts: vec![alice],
13389            data: execute_data.clone(),
13390        };
13391        let execute_tx = make_signed_tx(&alice_kp, execute_ix, genesis_hash);
13392        let execute_result = processor.process_transaction(&execute_tx, &validator);
13393        assert!(!execute_result.success);
13394        assert!(execute_result
13395            .error
13396            .as_deref()
13397            .unwrap_or("")
13398            .contains("timelocked"));
13399
13400        let fresh_blockhash = advance_test_slot(&state, SLOTS_PER_EPOCH);
13401        let execute_ix = Instruction {
13402            program_id: SYSTEM_PROGRAM_ID,
13403            accounts: vec![bob],
13404            data: execute_data,
13405        };
13406        let execute_tx = make_signed_tx(&bob_kp, execute_ix, fresh_blockhash);
13407        let execute_result = processor.process_transaction(&execute_tx, &validator);
13408        assert!(
13409            execute_result.success,
13410            "Execution should succeed: {:?}",
13411            execute_result.error
13412        );
13413
13414        assert_eq!(
13415            state
13416                .get_contract_storage(&contract_addr, b"last_caller")
13417                .unwrap()
13418                .unwrap(),
13419            gov.0.to_vec()
13420        );
13421        assert_eq!(
13422            state
13423                .get_contract_storage(&contract_addr, b"last_args")
13424                .unwrap()
13425                .unwrap(),
13426            call_args
13427        );
13428    }
13429
13430    #[test]
13431    fn test_amm_protocol_fee_collection_rejects_governance_authority_direct_path() {
13432        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13433        let validator = Pubkey([42u8; 32]);
13434        let bob = Pubkey([0x49; 32]);
13435        let gov = Pubkey([0x4A; 32]);
13436
13437        let fund = Account::licn_to_spores(1_000);
13438        state
13439            .put_account(&alice, &Account::new(fund, alice))
13440            .unwrap();
13441        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13442        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13443        state.set_governance_authority(&gov).unwrap();
13444        state
13445            .set_governed_wallet_config(
13446                &gov,
13447                &crate::multisig::GovernedWalletConfig::new(
13448                    2,
13449                    vec![alice, bob, gov],
13450                    "community_treasury",
13451                )
13452                .with_timelock(5),
13453            )
13454            .unwrap();
13455        configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob]);
13456
13457        let contract_addr =
13458            install_test_contract_account(&state, gov, governance_test_contract_code());
13459        register_contract_symbol_for_test(&state, gov, contract_addr, "DEXAMM");
13460
13461        let mut call_args = vec![0u8; 41];
13462        call_args[0] = 21u8;
13463        call_args[1] = 0x44;
13464        call_args[33..41].copy_from_slice(&7u64.to_le_bytes());
13465        let propose_ix = Instruction {
13466            program_id: SYSTEM_PROGRAM_ID,
13467            accounts: vec![alice, gov, contract_addr],
13468            data: make_governance_contract_call_data("call", &call_args, 0),
13469        };
13470        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13471        let propose_result = processor.process_transaction(&propose_tx, &validator);
13472        assert!(!propose_result.success);
13473        assert!(propose_result.error.as_deref().unwrap_or("").contains(
13474            "Protocol fund movement governance actions must use the treasury executor approval authority"
13475        ));
13476    }
13477
13478    #[test]
13479    fn test_protocol_outflow_contract_calls_reject_governance_authority_direct_path() {
13480        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13481        let validator = Pubkey([42u8; 32]);
13482        let bob = Pubkey([0x45; 32]);
13483        let gov = Pubkey([0x46; 32]);
13484
13485        let fund = Account::licn_to_spores(1_000);
13486        state
13487            .put_account(&alice, &Account::new(fund, alice))
13488            .unwrap();
13489        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13490        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13491        state.set_governance_authority(&gov).unwrap();
13492        state
13493            .set_governed_wallet_config(
13494                &gov,
13495                &crate::multisig::GovernedWalletConfig::new(
13496                    2,
13497                    vec![alice, bob, gov],
13498                    "community_treasury",
13499                )
13500                .with_timelock(5),
13501            )
13502            .unwrap();
13503        configure_treasury_executor_for_test(&state, gov, 2, vec![alice, bob]);
13504
13505        let contract_addr =
13506            install_test_contract_account(&state, gov, governance_test_contract_code());
13507        register_contract_symbol_for_test(&state, gov, contract_addr, "LEND");
13508
13509        let mut call_args = vec![0u8; 8];
13510        call_args.copy_from_slice(&500_000u64.to_le_bytes());
13511        let propose_ix = Instruction {
13512            program_id: SYSTEM_PROGRAM_ID,
13513            accounts: vec![alice, gov, contract_addr],
13514            data: make_governance_contract_call_data("withdraw_reserves", &call_args, 0),
13515        };
13516        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13517        let propose_result = processor.process_transaction(&propose_tx, &validator);
13518        assert!(!propose_result.success);
13519        assert!(propose_result.error.as_deref().unwrap_or("").contains(
13520            "Protocol fund movement governance actions must use the treasury executor approval authority"
13521        ));
13522    }
13523
13524    #[test]
13525    fn test_register_symbol_requires_governance_proposal_when_owner_is_governed() {
13526        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13527        let validator = Pubkey([42u8; 32]);
13528        let bob_kp = Keypair::generate();
13529        let bob = bob_kp.pubkey();
13530        let gov_kp = Keypair::generate();
13531        let gov = gov_kp.pubkey();
13532        let contract_id = Pubkey([0xA1; 32]);
13533
13534        let fund = Account::licn_to_spores(1_000);
13535        state
13536            .put_account(&alice, &Account::new(fund, alice))
13537            .unwrap();
13538        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13539        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13540        state.set_governance_authority(&gov).unwrap();
13541        state
13542            .set_governed_wallet_config(
13543                &gov,
13544                &crate::multisig::GovernedWalletConfig::new(
13545                    2,
13546                    vec![alice, bob, gov],
13547                    "community_treasury",
13548                ),
13549            )
13550            .unwrap();
13551        deploy_fake_contract(&state, gov, contract_id);
13552
13553        let json_payload = r#"{"symbol":"GOVSYM","name":"Governed Symbol","template":"token"}"#;
13554        let mut direct_data = vec![20u8];
13555        direct_data.extend_from_slice(json_payload.as_bytes());
13556        let direct_ix = Instruction {
13557            program_id: SYSTEM_PROGRAM_ID,
13558            accounts: vec![gov, contract_id],
13559            data: direct_data,
13560        };
13561        let direct_tx = make_signed_tx(&gov_kp, direct_ix, genesis_hash);
13562        let direct_result = processor.process_transaction(&direct_tx, &validator);
13563        assert!(!direct_result.success);
13564        assert!(direct_result
13565            .error
13566            .as_deref()
13567            .unwrap_or("")
13568            .contains("proposal flow"));
13569
13570        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_REGISTER_SYMBOL];
13571        propose_data.extend_from_slice(&(json_payload.len() as u32).to_le_bytes());
13572        propose_data.extend_from_slice(json_payload.as_bytes());
13573        let propose_ix = Instruction {
13574            program_id: SYSTEM_PROGRAM_ID,
13575            accounts: vec![alice, gov, contract_id],
13576            data: propose_data,
13577        };
13578        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13579        assert!(
13580            processor
13581                .process_transaction(&propose_tx, &validator)
13582                .success
13583        );
13584
13585        let mut approve_data = vec![35u8];
13586        approve_data.extend_from_slice(&1u64.to_le_bytes());
13587        let approve_ix = Instruction {
13588            program_id: SYSTEM_PROGRAM_ID,
13589            accounts: vec![bob],
13590            data: approve_data,
13591        };
13592        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13593        let approve_result = processor.process_transaction(&approve_tx, &validator);
13594        assert!(
13595            approve_result.success,
13596            "Approval should execute symbol registration: {:?}",
13597            approve_result.error
13598        );
13599
13600        let entry = state.get_symbol_registry("GOVSYM").unwrap().unwrap();
13601        assert_eq!(entry.program, contract_id);
13602        assert_eq!(entry.owner, gov);
13603
13604        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13605        assert_eq!(proposal.approval_authority, None);
13606        assert!(proposal.executed);
13607        assert_eq!(proposal.action_label, "register_contract_symbol");
13608    }
13609
13610    #[test]
13611    fn test_set_contract_abi_requires_governance_proposal_when_owner_is_governed() {
13612        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13613        let validator = Pubkey([42u8; 32]);
13614        let bob_kp = Keypair::generate();
13615        let bob = bob_kp.pubkey();
13616        let gov_kp = Keypair::generate();
13617        let gov = gov_kp.pubkey();
13618        let contract_id = Pubkey([0xA2; 32]);
13619
13620        let fund = Account::licn_to_spores(1_000);
13621        state
13622            .put_account(&alice, &Account::new(fund, alice))
13623            .unwrap();
13624        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13625        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13626        state.set_governance_authority(&gov).unwrap();
13627        state
13628            .set_governed_wallet_config(
13629                &gov,
13630                &crate::multisig::GovernedWalletConfig::new(
13631                    2,
13632                    vec![alice, bob, gov],
13633                    "community_treasury",
13634                ),
13635            )
13636            .unwrap();
13637        deploy_fake_contract(&state, gov, contract_id);
13638
13639        let abi = serde_json::json!({
13640            "version": "1.0",
13641            "name": "GovernedAbi",
13642            "functions": []
13643        });
13644        let abi_bytes = serde_json::to_vec(&abi).unwrap();
13645
13646        let mut direct_data = vec![18u8];
13647        direct_data.extend_from_slice(&abi_bytes);
13648        let direct_ix = Instruction {
13649            program_id: SYSTEM_PROGRAM_ID,
13650            accounts: vec![gov, contract_id],
13651            data: direct_data,
13652        };
13653        let direct_tx = make_signed_tx(&gov_kp, direct_ix, genesis_hash);
13654        let direct_result = processor.process_transaction(&direct_tx, &validator);
13655        assert!(!direct_result.success);
13656        assert!(direct_result
13657            .error
13658            .as_deref()
13659            .unwrap_or("")
13660            .contains("proposal flow"));
13661
13662        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_SET_CONTRACT_ABI];
13663        propose_data.extend_from_slice(&(abi_bytes.len() as u32).to_le_bytes());
13664        propose_data.extend_from_slice(&abi_bytes);
13665        let propose_ix = Instruction {
13666            program_id: SYSTEM_PROGRAM_ID,
13667            accounts: vec![alice, gov, contract_id],
13668            data: propose_data,
13669        };
13670        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13671        assert!(
13672            processor
13673                .process_transaction(&propose_tx, &validator)
13674                .success
13675        );
13676
13677        let mut approve_data = vec![35u8];
13678        approve_data.extend_from_slice(&1u64.to_le_bytes());
13679        let approve_ix = Instruction {
13680            program_id: SYSTEM_PROGRAM_ID,
13681            accounts: vec![bob],
13682            data: approve_data,
13683        };
13684        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13685        let approve_result = processor.process_transaction(&approve_tx, &validator);
13686        assert!(
13687            approve_result.success,
13688            "Approval should execute ABI update: {:?}",
13689            approve_result.error
13690        );
13691
13692        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13693        assert_eq!(proposal.approval_authority, None);
13694
13695        let acct = state.get_account(&contract_id).unwrap().unwrap();
13696        let contract: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
13697        let abi = contract.abi.expect("governance proposal should set ABI");
13698        assert_eq!(abi.name, "GovernedAbi");
13699    }
13700
13701    #[test]
13702    fn test_contract_close_owner_semantics_preserved_for_non_active_lifecycle_statuses() {
13703        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13704        let validator = Pubkey([42u8; 32]);
13705
13706        for (idx, status) in [
13707            crate::ContractLifecycleStatus::Suspended,
13708            crate::ContractLifecycleStatus::Quarantined,
13709            crate::ContractLifecycleStatus::Terminated,
13710        ]
13711        .into_iter()
13712        .enumerate()
13713        {
13714            let contract_id = Pubkey([0xB0 + idx as u8; 32]);
13715            let destination = Pubkey([0xC0 + idx as u8; 32]);
13716            let close_amount = Account::licn_to_spores(10 + idx as u64);
13717
13718            deploy_fake_contract(&state, alice, contract_id);
13719            set_contract_lifecycle_status_for_test(&state, contract_id, status);
13720            let mut contract_account = state.get_account(&contract_id).unwrap().unwrap();
13721            contract_account.spores = close_amount;
13722            contract_account.spendable = close_amount;
13723            state.put_account(&contract_id, &contract_account).unwrap();
13724
13725            let result = submit_contract_ix(
13726                &processor,
13727                &alice_kp,
13728                vec![alice, contract_id, destination],
13729                crate::ContractInstruction::Close,
13730                genesis_hash,
13731                &validator,
13732            );
13733
13734            assert!(
13735                result.success,
13736                "owner close should succeed for {:?}: {:?}",
13737                status, result.error
13738            );
13739            let closed = state.get_account(&contract_id).unwrap().unwrap();
13740            assert!(!closed.executable);
13741            assert!(closed.data.is_empty());
13742            assert_eq!(closed.spendable, 0);
13743            assert_eq!(state.get_balance(&destination).unwrap(), close_amount);
13744        }
13745    }
13746
13747    #[test]
13748    fn test_contract_close_non_owner_still_rejected_for_non_active_lifecycle_contract() {
13749        let (processor, state, _alice_kp, alice, _treasury, genesis_hash) = setup();
13750        let validator = Pubkey([42u8; 32]);
13751        let eve_kp = Keypair::generate();
13752        let eve = eve_kp.pubkey();
13753        let contract_id = Pubkey([0xB5; 32]);
13754        let destination = Pubkey([0xC5; 32]);
13755
13756        state
13757            .put_account(&eve, &Account::new(Account::licn_to_spores(1_000), eve))
13758            .unwrap();
13759        deploy_fake_contract(&state, alice, contract_id);
13760        set_contract_lifecycle_status_for_test(
13761            &state,
13762            contract_id,
13763            crate::ContractLifecycleStatus::Terminated,
13764        );
13765        let mut contract_account = state.get_account(&contract_id).unwrap().unwrap();
13766        contract_account.spores = Account::licn_to_spores(15);
13767        contract_account.spendable = Account::licn_to_spores(15);
13768        state.put_account(&contract_id, &contract_account).unwrap();
13769
13770        let result = submit_contract_ix(
13771            &processor,
13772            &eve_kp,
13773            vec![eve, contract_id, destination],
13774            crate::ContractInstruction::Close,
13775            genesis_hash,
13776            &validator,
13777        );
13778
13779        assert!(!result.success);
13780        assert!(result
13781            .error
13782            .as_deref()
13783            .unwrap_or("")
13784            .contains("Only contract owner can close"));
13785        let contract_account = state.get_account(&contract_id).unwrap().unwrap();
13786        assert!(contract_account.executable);
13787        let contract: crate::ContractAccount =
13788            serde_json::from_slice(&contract_account.data).unwrap();
13789        assert_eq!(
13790            contract.lifecycle_status,
13791            crate::ContractLifecycleStatus::Terminated
13792        );
13793        assert_eq!(state.get_balance(&destination).unwrap_or(0), 0);
13794    }
13795
13796    #[test]
13797    fn test_contract_close_requires_governance_proposal_when_owner_is_governed() {
13798        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13799        let validator = Pubkey([42u8; 32]);
13800        let bob_kp = Keypair::generate();
13801        let bob = bob_kp.pubkey();
13802        let gov_kp = Keypair::generate();
13803        let gov = gov_kp.pubkey();
13804        let contract_id = Pubkey([0xA3; 32]);
13805        let destination = Pubkey([0xA4; 32]);
13806
13807        let fund = Account::licn_to_spores(1_000);
13808        state
13809            .put_account(&alice, &Account::new(fund, alice))
13810            .unwrap();
13811        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13812        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13813        state.set_governance_authority(&gov).unwrap();
13814        state
13815            .set_governed_wallet_config(
13816                &gov,
13817                &crate::multisig::GovernedWalletConfig::new(
13818                    2,
13819                    vec![alice, bob, gov],
13820                    "community_treasury",
13821                ),
13822            )
13823            .unwrap();
13824        deploy_fake_contract(&state, gov, contract_id);
13825        set_contract_lifecycle_status_for_test(
13826            &state,
13827            contract_id,
13828            crate::ContractLifecycleStatus::Quarantined,
13829        );
13830
13831        let mut contract_account = state.get_account(&contract_id).unwrap().unwrap();
13832        contract_account.spores = Account::licn_to_spores(25);
13833        contract_account.spendable = Account::licn_to_spores(25);
13834        state.put_account(&contract_id, &contract_account).unwrap();
13835
13836        let direct_ix = Instruction {
13837            program_id: CONTRACT_PROGRAM_ID,
13838            accounts: vec![gov, contract_id, destination],
13839            data: crate::ContractInstruction::Close.serialize().unwrap(),
13840        };
13841        let direct_tx = make_signed_tx(&gov_kp, direct_ix, genesis_hash);
13842        let direct_result = processor.process_transaction(&direct_tx, &validator);
13843        assert!(!direct_result.success);
13844        assert!(direct_result
13845            .error
13846            .as_deref()
13847            .unwrap_or("")
13848            .contains("proposal flow"));
13849
13850        let propose_ix = Instruction {
13851            program_id: SYSTEM_PROGRAM_ID,
13852            accounts: vec![alice, gov, contract_id, destination],
13853            data: vec![34u8, GOVERNANCE_ACTION_CONTRACT_CLOSE],
13854        };
13855        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13856        assert!(
13857            processor
13858                .process_transaction(&propose_tx, &validator)
13859                .success
13860        );
13861
13862        let mut approve_data = vec![35u8];
13863        approve_data.extend_from_slice(&1u64.to_le_bytes());
13864        let approve_ix = Instruction {
13865            program_id: SYSTEM_PROGRAM_ID,
13866            accounts: vec![bob],
13867            data: approve_data,
13868        };
13869        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13870        let approve_result = processor.process_transaction(&approve_tx, &validator);
13871        assert!(
13872            approve_result.success,
13873            "Approval should execute contract close: {:?}",
13874            approve_result.error
13875        );
13876
13877        let proposal = state.get_governance_proposal(1).unwrap().unwrap();
13878        assert_eq!(proposal.approval_authority, None);
13879
13880        let closed = state.get_account(&contract_id).unwrap().unwrap();
13881        assert!(!closed.executable);
13882        assert!(closed.data.is_empty());
13883        assert_eq!(
13884            state.get_balance(&destination).unwrap(),
13885            Account::licn_to_spores(25)
13886        );
13887    }
13888
13889    #[test]
13890    fn test_governance_proposal_lifecycle_events_are_emitted() {
13891        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13892        let validator = Pubkey([42u8; 32]);
13893        let bob_kp = Keypair::generate();
13894        let bob = bob_kp.pubkey();
13895        let gov = Pubkey([0xA6; 32]);
13896        let recipient = Pubkey([0xA5; 32]);
13897        let treasury_authority = crate::multisig::derive_treasury_executor_authority(&gov);
13898
13899        let fund = Account::licn_to_spores(1_000);
13900        state
13901            .put_account(&alice, &Account::new(fund, alice))
13902            .unwrap();
13903        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13904        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13905        state.set_governance_authority(&gov).unwrap();
13906        state
13907            .set_governed_wallet_config(
13908                &gov,
13909                &crate::multisig::GovernedWalletConfig::new(
13910                    2,
13911                    vec![alice, bob],
13912                    "community_treasury",
13913                ),
13914            )
13915            .unwrap();
13916        state
13917            .set_treasury_executor_authority(&treasury_authority)
13918            .unwrap();
13919        state
13920            .set_governed_wallet_config(
13921                &treasury_authority,
13922                &crate::multisig::GovernedWalletConfig::new(
13923                    2,
13924                    vec![alice, bob],
13925                    crate::multisig::TREASURY_EXECUTOR_LABEL,
13926                ),
13927            )
13928            .unwrap();
13929
13930        let amount = Account::licn_to_spores(10);
13931        let mut propose_data = vec![34u8, GOVERNANCE_ACTION_TREASURY_TRANSFER];
13932        propose_data.extend_from_slice(&amount.to_le_bytes());
13933        let propose_ix = Instruction {
13934            program_id: SYSTEM_PROGRAM_ID,
13935            accounts: vec![alice, treasury_authority, recipient],
13936            data: propose_data,
13937        };
13938        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
13939        assert!(
13940            processor
13941                .process_transaction(&propose_tx, &validator)
13942                .success
13943        );
13944
13945        let mut approve_data = vec![35u8];
13946        approve_data.extend_from_slice(&1u64.to_le_bytes());
13947        let approve_ix = Instruction {
13948            program_id: SYSTEM_PROGRAM_ID,
13949            accounts: vec![bob],
13950            data: approve_data,
13951        };
13952        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
13953        assert!(
13954            processor
13955                .process_transaction(&approve_tx, &validator)
13956                .success
13957        );
13958
13959        let events = state
13960            .get_events_by_program(&SYSTEM_PROGRAM_ID, 10, None)
13961            .unwrap();
13962        let proposal_events: Vec<_> = events
13963            .into_iter()
13964            .filter(|event| event.data.get("proposal_id").map(String::as_str) == Some("1"))
13965            .collect();
13966
13967        let event_names: Vec<_> = proposal_events
13968            .iter()
13969            .map(|event| event.name.as_str())
13970            .collect();
13971        assert!(event_names.contains(&"GovernanceProposalCreated"));
13972        assert!(event_names.contains(&"GovernanceProposalApproved"));
13973        assert!(event_names.contains(&"GovernanceProposalExecuted"));
13974        assert!(proposal_events
13975            .iter()
13976            .all(|event| event.program == SYSTEM_PROGRAM_ID));
13977        assert!(proposal_events
13978            .iter()
13979            .all(|event| { event.data.get("action") == Some(&"treasury_transfer".to_string()) }));
13980    }
13981
13982    #[test]
13983    fn test_governance_contract_call_events_include_structured_call_hints() {
13984        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
13985        let validator = Pubkey([42u8; 32]);
13986        let bob_kp = Keypair::generate();
13987        let bob = bob_kp.pubkey();
13988        let gov = Pubkey([0xA7; 32]);
13989
13990        let fund = Account::licn_to_spores(1_000);
13991        state
13992            .put_account(&alice, &Account::new(fund, alice))
13993            .unwrap();
13994        state.put_account(&bob, &Account::new(fund, bob)).unwrap();
13995        state.put_account(&gov, &Account::new(fund, gov)).unwrap();
13996        state.set_governance_authority(&gov).unwrap();
13997        state
13998            .set_governed_wallet_config(
13999                &gov,
14000                &crate::multisig::GovernedWalletConfig::new(
14001                    2,
14002                    vec![alice, bob],
14003                    "community_treasury",
14004                ),
14005            )
14006            .unwrap();
14007
14008        let contract_addr =
14009            install_test_contract_account(&state, alice, governance_test_contract_code());
14010        let call_args = vec![0xAA, 0xBB, 0xCC, 0xDD];
14011        let call_value = 7u64;
14012
14013        let propose_ix = Instruction {
14014            program_id: SYSTEM_PROGRAM_ID,
14015            accounts: vec![alice, gov, contract_addr],
14016            data: make_governance_contract_call_data("record_call", &call_args, call_value),
14017        };
14018        let propose_tx = make_signed_tx(&alice_kp, propose_ix, genesis_hash);
14019        assert!(
14020            processor
14021                .process_transaction(&propose_tx, &validator)
14022                .success
14023        );
14024
14025        let mut approve_data = vec![35u8];
14026        approve_data.extend_from_slice(&1u64.to_le_bytes());
14027        let approve_ix = Instruction {
14028            program_id: SYSTEM_PROGRAM_ID,
14029            accounts: vec![bob],
14030            data: approve_data,
14031        };
14032        let approve_tx = make_signed_tx(&bob_kp, approve_ix, genesis_hash);
14033        assert!(
14034            processor
14035                .process_transaction(&approve_tx, &validator)
14036                .success
14037        );
14038
14039        let events = state
14040            .get_events_by_program(&SYSTEM_PROGRAM_ID, 10, None)
14041            .unwrap();
14042        let proposal_events: Vec<_> = events
14043            .into_iter()
14044            .filter(|event| event.data.get("proposal_id").map(String::as_str) == Some("1"))
14045            .collect();
14046
14047        let event_names: Vec<_> = proposal_events
14048            .iter()
14049            .map(|event| event.name.as_str())
14050            .collect();
14051        assert!(event_names.contains(&"GovernanceProposalCreated"));
14052        assert!(event_names.contains(&"GovernanceProposalApproved"));
14053        assert!(event_names.contains(&"GovernanceProposalExecuted"));
14054
14055        let target_contract = contract_addr.to_base58();
14056        let call_args_len = call_args.len().to_string();
14057        let call_value = call_value.to_string();
14058        assert!(proposal_events.iter().all(|event| {
14059            event.data.get("target_contract") == Some(&target_contract)
14060                && event.data.get("target_function") == Some(&"record_call".to_string())
14061                && event.data.get("call_args_len") == Some(&call_args_len)
14062                && event.data.get("call_value_spores") == Some(&call_value)
14063        }));
14064    }
14065
14066    #[test]
14067    fn test_upgrade_timelock_rejects_double_stage() {
14068        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14069        let validator = Pubkey([42u8; 32]);
14070
14071        let contract_addr = deploy_test_contract(
14072            &processor,
14073            &state,
14074            &alice_kp,
14075            alice,
14076            genesis_hash,
14077            &validator,
14078        );
14079
14080        // Set timelock
14081        let r = submit_contract_ix(
14082            &processor,
14083            &alice_kp,
14084            vec![alice, contract_addr],
14085            crate::ContractInstruction::SetUpgradeTimelock { epochs: 2 },
14086            genesis_hash,
14087            &validator,
14088        );
14089        assert!(r.success);
14090
14091        // First upgrade → staged
14092        let r = submit_contract_ix(
14093            &processor,
14094            &alice_kp,
14095            vec![alice, contract_addr],
14096            crate::ContractInstruction::Upgrade {
14097                code: valid_wasm_code(0x03),
14098            },
14099            genesis_hash,
14100            &validator,
14101        );
14102        assert!(r.success);
14103
14104        // Second upgrade while first is pending → should fail
14105        let r = submit_contract_ix(
14106            &processor,
14107            &alice_kp,
14108            vec![alice, contract_addr],
14109            crate::ContractInstruction::Upgrade {
14110                code: valid_wasm_code(0x04),
14111            },
14112            genesis_hash,
14113            &validator,
14114        );
14115        assert!(!r.success, "Double-stage should be rejected");
14116        assert!(r
14117            .error
14118            .as_deref()
14119            .unwrap_or("")
14120            .contains("already has a pending upgrade"));
14121    }
14122
14123    #[test]
14124    fn test_execute_upgrade_before_timelock_expires_fails() {
14125        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
14126        let validator = Pubkey([42u8; 32]);
14127
14128        let contract_addr = deploy_test_contract(
14129            &processor,
14130            &_state,
14131            &alice_kp,
14132            alice,
14133            genesis_hash,
14134            &validator,
14135        );
14136
14137        // Set 5-epoch timelock (current slot = 0 → epoch 0, needs > epoch 5)
14138        let r = submit_contract_ix(
14139            &processor,
14140            &alice_kp,
14141            vec![alice, contract_addr],
14142            crate::ContractInstruction::SetUpgradeTimelock { epochs: 5 },
14143            genesis_hash,
14144            &validator,
14145        );
14146        assert!(r.success);
14147
14148        // Stage upgrade
14149        let r = submit_contract_ix(
14150            &processor,
14151            &alice_kp,
14152            vec![alice, contract_addr],
14153            crate::ContractInstruction::Upgrade {
14154                code: valid_wasm_code(0x05),
14155            },
14156            genesis_hash,
14157            &validator,
14158        );
14159        assert!(r.success);
14160
14161        // Try execute immediately (epoch 0, needs > epoch 5) → should fail
14162        let r = submit_contract_ix(
14163            &processor,
14164            &alice_kp,
14165            vec![alice, contract_addr],
14166            crate::ContractInstruction::ExecuteUpgrade,
14167            genesis_hash,
14168            &validator,
14169        );
14170        assert!(!r.success, "Should fail: timelock not expired");
14171        assert!(r
14172            .error
14173            .as_deref()
14174            .unwrap_or("")
14175            .contains("Timelock has not expired"));
14176    }
14177
14178    #[test]
14179    fn test_execute_upgrade_no_pending_fails() {
14180        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
14181        let validator = Pubkey([42u8; 32]);
14182
14183        let contract_addr = deploy_test_contract(
14184            &processor,
14185            &_state,
14186            &alice_kp,
14187            alice,
14188            genesis_hash,
14189            &validator,
14190        );
14191
14192        // Try execute with no pending upgrade → should fail
14193        let r = submit_contract_ix(
14194            &processor,
14195            &alice_kp,
14196            vec![alice, contract_addr],
14197            crate::ContractInstruction::ExecuteUpgrade,
14198            genesis_hash,
14199            &validator,
14200        );
14201        assert!(!r.success, "Should fail: no pending upgrade");
14202        assert!(r
14203            .error
14204            .as_deref()
14205            .unwrap_or("")
14206            .contains("No pending upgrade"));
14207    }
14208
14209    #[test]
14210    fn test_veto_upgrade_by_governance_authority() {
14211        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14212        let validator = Pubkey([42u8; 32]);
14213
14214        let contract_addr = deploy_test_contract(
14215            &processor,
14216            &state,
14217            &alice_kp,
14218            alice,
14219            genesis_hash,
14220            &validator,
14221        );
14222
14223        // Set governance authority
14224        let gov_kp = crate::Keypair::generate();
14225        let gov = gov_kp.pubkey();
14226        state.set_governance_authority(&gov).unwrap();
14227        // Fund governance account (10 LICN)
14228        let gov_acct = crate::Account::new(10, gov);
14229        state.put_account(&gov, &gov_acct).unwrap();
14230
14231        // Set timelock + stage upgrade
14232        let r = submit_contract_ix(
14233            &processor,
14234            &alice_kp,
14235            vec![alice, contract_addr],
14236            crate::ContractInstruction::SetUpgradeTimelock { epochs: 2 },
14237            genesis_hash,
14238            &validator,
14239        );
14240        assert!(r.success);
14241
14242        let r = submit_contract_ix(
14243            &processor,
14244            &alice_kp,
14245            vec![alice, contract_addr],
14246            crate::ContractInstruction::Upgrade {
14247                code: valid_wasm_code(0x06),
14248            },
14249            genesis_hash,
14250            &validator,
14251        );
14252        assert!(r.success);
14253
14254        // Verify pending exists
14255        let acct = state.get_account(&contract_addr).unwrap().unwrap();
14256        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
14257        assert!(ca.pending_upgrade.is_some());
14258
14259        // Governance authority vetoes
14260        let r = submit_contract_ix(
14261            &processor,
14262            &gov_kp,
14263            vec![gov, contract_addr],
14264            crate::ContractInstruction::VetoUpgrade,
14265            genesis_hash,
14266            &validator,
14267        );
14268        assert!(r.success, "Veto should succeed: {:?}", r.error);
14269
14270        // Verify pending is cleared
14271        let acct = state.get_account(&contract_addr).unwrap().unwrap();
14272        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
14273        assert!(
14274            ca.pending_upgrade.is_none(),
14275            "Pending upgrade should be cleared"
14276        );
14277        assert_eq!(ca.version, 1, "Version should NOT change after veto");
14278    }
14279
14280    #[test]
14281    fn test_veto_by_non_governance_fails() {
14282        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14283        let validator = Pubkey([42u8; 32]);
14284
14285        let contract_addr = deploy_test_contract(
14286            &processor,
14287            &state,
14288            &alice_kp,
14289            alice,
14290            genesis_hash,
14291            &validator,
14292        );
14293
14294        // Set governance authority to someone else
14295        let gov_kp = crate::Keypair::generate();
14296        let gov = gov_kp.pubkey();
14297        state.set_governance_authority(&gov).unwrap();
14298
14299        // Set timelock + stage upgrade
14300        let r = submit_contract_ix(
14301            &processor,
14302            &alice_kp,
14303            vec![alice, contract_addr],
14304            crate::ContractInstruction::SetUpgradeTimelock { epochs: 1 },
14305            genesis_hash,
14306            &validator,
14307        );
14308        assert!(r.success);
14309
14310        let r = submit_contract_ix(
14311            &processor,
14312            &alice_kp,
14313            vec![alice, contract_addr],
14314            crate::ContractInstruction::Upgrade {
14315                code: valid_wasm_code(0x07),
14316            },
14317            genesis_hash,
14318            &validator,
14319        );
14320        assert!(r.success);
14321
14322        // Alice (not governance) tries to veto → should fail
14323        let r = submit_contract_ix(
14324            &processor,
14325            &alice_kp,
14326            vec![alice, contract_addr],
14327            crate::ContractInstruction::VetoUpgrade,
14328            genesis_hash,
14329            &validator,
14330        );
14331        assert!(!r.success, "Non-governance should not be able to veto");
14332        assert!(r
14333            .error
14334            .as_deref()
14335            .unwrap_or("")
14336            .contains("governance authority"));
14337    }
14338
14339    #[test]
14340    fn test_cannot_remove_timelock_while_upgrade_pending() {
14341        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
14342        let validator = Pubkey([42u8; 32]);
14343
14344        let contract_addr = deploy_test_contract(
14345            &processor,
14346            &_state,
14347            &alice_kp,
14348            alice,
14349            genesis_hash,
14350            &validator,
14351        );
14352
14353        // Set timelock
14354        let r = submit_contract_ix(
14355            &processor,
14356            &alice_kp,
14357            vec![alice, contract_addr],
14358            crate::ContractInstruction::SetUpgradeTimelock { epochs: 2 },
14359            genesis_hash,
14360            &validator,
14361        );
14362        assert!(r.success);
14363
14364        // Stage upgrade
14365        let r = submit_contract_ix(
14366            &processor,
14367            &alice_kp,
14368            vec![alice, contract_addr],
14369            crate::ContractInstruction::Upgrade {
14370                code: valid_wasm_code(0x08),
14371            },
14372            genesis_hash,
14373            &validator,
14374        );
14375        assert!(r.success);
14376
14377        // Try to remove timelock while upgrade is pending → should fail
14378        let r = submit_contract_ix(
14379            &processor,
14380            &alice_kp,
14381            vec![alice, contract_addr],
14382            crate::ContractInstruction::SetUpgradeTimelock { epochs: 0 },
14383            genesis_hash,
14384            &validator,
14385        );
14386        assert!(
14387            !r.success,
14388            "Should not remove timelock while upgrade pending"
14389        );
14390        assert!(r.error.as_deref().unwrap_or("").contains("pending"));
14391    }
14392
14393    #[test]
14394    fn test_set_timelock_zero_removes_it() {
14395        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14396        let validator = Pubkey([42u8; 32]);
14397
14398        let contract_addr = deploy_test_contract(
14399            &processor,
14400            &state,
14401            &alice_kp,
14402            alice,
14403            genesis_hash,
14404            &validator,
14405        );
14406
14407        // Set timelock
14408        let r = submit_contract_ix(
14409            &processor,
14410            &alice_kp,
14411            vec![alice, contract_addr],
14412            crate::ContractInstruction::SetUpgradeTimelock { epochs: 5 },
14413            genesis_hash,
14414            &validator,
14415        );
14416        assert!(r.success);
14417
14418        let acct = state.get_account(&contract_addr).unwrap().unwrap();
14419        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
14420        assert_eq!(ca.upgrade_timelock_epochs, Some(5));
14421
14422        // Remove timelock (no pending upgrade)
14423        let r = submit_contract_ix(
14424            &processor,
14425            &alice_kp,
14426            vec![alice, contract_addr],
14427            crate::ContractInstruction::SetUpgradeTimelock { epochs: 0 },
14428            genesis_hash,
14429            &validator,
14430        );
14431        assert!(r.success, "Remove timelock should succeed: {:?}", r.error);
14432
14433        let acct = state.get_account(&contract_addr).unwrap().unwrap();
14434        let ca: crate::ContractAccount = serde_json::from_slice(&acct.data).unwrap();
14435        assert_eq!(ca.upgrade_timelock_epochs, None);
14436    }
14437
14438    #[test]
14439    fn test_contract_account_serde_backward_compat_no_timelock() {
14440        // Legacy contract data without timelock fields should deserialize with defaults
14441        let owner_bytes: Vec<u8> = vec![1u8; 32];
14442        let hash_bytes: Vec<u8> = vec![0u8; 32];
14443        let json = serde_json::json!({
14444            "code": [0, 0x61, 0x73, 0x6D],
14445            "storage": {},
14446            "owner": owner_bytes,
14447            "code_hash": hash_bytes,
14448            "version": 1
14449        });
14450        let ca: crate::ContractAccount = serde_json::from_value(json).unwrap();
14451        assert_eq!(ca.upgrade_timelock_epochs, None);
14452        assert!(ca.pending_upgrade.is_none());
14453    }
14454
14455    // ─── CU Budget & Priority Fee Tests ───────────────────────────────
14456
14457    /// Helper: build a transfer TX with custom compute_budget and compute_unit_price
14458    fn make_transfer_tx_with_cu(
14459        from_kp: &Keypair,
14460        from: Pubkey,
14461        to: Pubkey,
14462        amount_licn: u64,
14463        recent_blockhash: Hash,
14464        compute_budget: Option<u64>,
14465        compute_unit_price: Option<u64>,
14466    ) -> Transaction {
14467        let mut data = vec![0u8];
14468        data.extend_from_slice(&Account::licn_to_spores(amount_licn).to_le_bytes());
14469
14470        let ix = Instruction {
14471            program_id: SYSTEM_PROGRAM_ID,
14472            accounts: vec![from, to],
14473            data,
14474        };
14475
14476        let mut message = crate::transaction::Message::new(vec![ix], recent_blockhash);
14477        message.compute_budget = compute_budget;
14478        message.compute_unit_price = compute_unit_price;
14479        let mut tx = Transaction::new(message);
14480        let sig = from_kp.sign(&tx.message.serialize());
14481        tx.signatures.push(sig);
14482        tx
14483    }
14484
14485    #[test]
14486    fn test_default_compute_budget_applied() {
14487        let msg = crate::transaction::Message::new(vec![], Hash::default());
14488        assert_eq!(
14489            msg.effective_compute_budget(),
14490            crate::transaction::DEFAULT_COMPUTE_BUDGET
14491        );
14492    }
14493
14494    #[test]
14495    fn test_custom_compute_budget_applied() {
14496        let mut msg = crate::transaction::Message::new(vec![], Hash::default());
14497        msg.compute_budget = Some(500_000);
14498        assert_eq!(msg.effective_compute_budget(), 500_000);
14499    }
14500
14501    #[test]
14502    fn test_compute_budget_capped_at_max() {
14503        let mut msg = crate::transaction::Message::new(vec![], Hash::default());
14504        msg.compute_budget = Some(2_000_000);
14505        assert_eq!(
14506            msg.effective_compute_budget(),
14507            crate::transaction::MAX_COMPUTE_BUDGET
14508        );
14509    }
14510
14511    #[test]
14512    fn test_zero_compute_budget_uses_default() {
14513        let mut msg = crate::transaction::Message::new(vec![], Hash::default());
14514        msg.compute_budget = Some(0);
14515        assert_eq!(
14516            msg.effective_compute_budget(),
14517            crate::transaction::DEFAULT_COMPUTE_BUDGET
14518        );
14519    }
14520
14521    #[test]
14522    fn test_priority_fee_computation_zero_price() {
14523        let (_, _, alice_kp, alice, _, genesis_hash) = setup();
14524        let bob = Pubkey([2u8; 32]);
14525        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
14526        let priority = TxProcessor::compute_priority_fee(&tx);
14527        assert_eq!(priority, 0);
14528    }
14529
14530    #[test]
14531    fn test_priority_fee_computation_with_price() {
14532        let (_, _, alice_kp, alice, _, genesis_hash) = setup();
14533        let bob = Pubkey([2u8; 32]);
14534        // compute_unit_price = 1000 μspores/CU, budget = 200_000 CU (default)
14535        // priority = 200_000 × 1000 / 1_000_000 = 200 spores
14536        let tx =
14537            make_transfer_tx_with_cu(&alice_kp, alice, bob, 10, genesis_hash, None, Some(1000));
14538        let priority = TxProcessor::compute_priority_fee(&tx);
14539        assert_eq!(priority, 200);
14540    }
14541
14542    #[test]
14543    fn test_priority_fee_with_custom_budget() {
14544        let (_, _, alice_kp, alice, _, genesis_hash) = setup();
14545        let bob = Pubkey([2u8; 32]);
14546        // compute_unit_price = 5000 μspores/CU, budget = 400_000 CU
14547        // priority = 400_000 × 5000 / 1_000_000 = 2000 spores
14548        let tx = make_transfer_tx_with_cu(
14549            &alice_kp,
14550            alice,
14551            bob,
14552            10,
14553            genesis_hash,
14554            Some(400_000),
14555            Some(5000),
14556        );
14557        let priority = TxProcessor::compute_priority_fee(&tx);
14558        assert_eq!(priority, 2000);
14559    }
14560
14561    #[test]
14562    fn test_total_fee_includes_priority() {
14563        let (_, _, alice_kp, alice, _, genesis_hash) = setup();
14564        let bob = Pubkey([2u8; 32]);
14565        let fee_config = FeeConfig::default_from_constants();
14566
14567        let tx_no_prio = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
14568        let base = TxProcessor::compute_base_fee(&tx_no_prio, &fee_config);
14569        let total_no_prio = TxProcessor::compute_transaction_fee(&tx_no_prio, &fee_config);
14570        assert_eq!(total_no_prio, base);
14571
14572        let tx_with_prio =
14573            make_transfer_tx_with_cu(&alice_kp, alice, bob, 10, genesis_hash, None, Some(1000));
14574        let total_with_prio = TxProcessor::compute_transaction_fee(&tx_with_prio, &fee_config);
14575        let priority = TxProcessor::compute_priority_fee(&tx_with_prio);
14576        assert_eq!(total_with_prio, base + priority);
14577        assert!(total_with_prio > total_no_prio);
14578    }
14579
14580    #[test]
14581    fn test_priority_fee_charged_on_transfer() {
14582        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14583        let bob = Pubkey([2u8; 32]);
14584        let validator = Pubkey([42u8; 32]);
14585
14586        let initial_balance = state.get_balance(&alice).unwrap();
14587        let transfer_amount = Account::licn_to_spores(10);
14588
14589        // cu_price=1000 μspores/CU, default budget=200K → priority=200 spores
14590        let tx =
14591            make_transfer_tx_with_cu(&alice_kp, alice, bob, 10, genesis_hash, None, Some(1000));
14592
14593        let fee_config = FeeConfig::default_from_constants();
14594        let expected_total = TxProcessor::compute_transaction_fee(&tx, &fee_config);
14595        let expected_priority = TxProcessor::compute_priority_fee(&tx);
14596        assert_eq!(expected_priority, 200);
14597
14598        let result = processor.process_transaction(&tx, &validator);
14599        assert!(result.success);
14600        assert_eq!(result.fee_paid, expected_total);
14601
14602        let final_balance = state.get_balance(&alice).unwrap();
14603        assert_eq!(
14604            final_balance,
14605            initial_balance - transfer_amount - expected_total
14606        );
14607    }
14608
14609    #[test]
14610    fn test_speculative_fee_commit_updates_burned_counter() {
14611        let (_processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14612        let speculative_processor = TxProcessor::new_speculative(state.clone());
14613        let bob = Pubkey([2u8; 32]);
14614        let validator = Pubkey([42u8; 32]);
14615
14616        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
14617        let fee_config = state.get_fee_config().unwrap();
14618        let expected_fee = TxProcessor::compute_transaction_fee(&tx, &fee_config);
14619        let expected_burn =
14620            (expected_fee as u128 * fee_config.fee_burn_percent as u128 / 100) as u64;
14621
14622        let execution = speculative_processor.process_transactions_speculative(&[tx], &validator);
14623        assert_eq!(execution.results.len(), 1);
14624        assert!(
14625            execution.results[0].success,
14626            "speculative transfer should succeed: {:?}",
14627            execution.results[0].error
14628        );
14629        assert_eq!(state.get_total_burned().unwrap(), 0);
14630
14631        state.commit_batch(execution.batch).unwrap();
14632
14633        assert_eq!(state.get_total_burned().unwrap(), expected_burn);
14634    }
14635
14636    #[test]
14637    fn test_fee_debit_bypasses_account_and_native_asset_restrictions() {
14638        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14639        let validator = Pubkey([42u8; 32]);
14640        let spendable = state.get_account(&alice).unwrap().unwrap().spendable;
14641
14642        put_active_processor_test_restriction(
14643            &state,
14644            RestrictionTarget::Account(alice),
14645            RestrictionMode::OutgoingOnly,
14646        );
14647        put_active_processor_test_restriction(
14648            &state,
14649            RestrictionTarget::AccountAsset {
14650                account: alice,
14651                asset: NATIVE_LICN_ASSET_ID,
14652            },
14653            RestrictionMode::FrozenAmount { amount: spendable },
14654        );
14655
14656        let evm_address = [0xAB; 20];
14657        let mut data = vec![12u8];
14658        data.extend_from_slice(&evm_address);
14659        let ix = Instruction {
14660            program_id: SYSTEM_PROGRAM_ID,
14661            accounts: vec![alice],
14662            data,
14663        };
14664        let before_balance = state.get_balance(&alice).unwrap();
14665        let tx = make_signed_tx(&alice_kp, ix, genesis_hash);
14666        let result = processor.process_transaction(&tx, &validator);
14667        assert!(
14668            result.success,
14669            "restricted signer should still pay fee for non-value action: {:?}",
14670            result.error
14671        );
14672
14673        let after_balance = state.get_balance(&alice).unwrap();
14674        assert_eq!(before_balance - after_balance, result.fee_paid);
14675        assert_eq!(state.lookup_evm_address(&evm_address).unwrap(), Some(alice));
14676    }
14677
14678    #[test]
14679    fn test_restricted_transfer_failure_keeps_only_fee_debit() {
14680        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14681        let bob = Pubkey([0xB4; 32]);
14682        let validator = Pubkey([42u8; 32]);
14683
14684        put_active_processor_test_restriction(
14685            &state,
14686            RestrictionTarget::Account(alice),
14687            RestrictionMode::OutgoingOnly,
14688        );
14689
14690        let before_balance = state.get_balance(&alice).unwrap();
14691        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
14692        let result = processor.process_transaction(&tx, &validator);
14693        assert!(!result.success);
14694        assert!(result
14695            .error
14696            .as_deref()
14697            .unwrap_or("")
14698            .contains("Native transfer blocked by active sender account restriction"));
14699
14700        let after_balance = state.get_balance(&alice).unwrap();
14701        assert_eq!(before_balance - after_balance, result.fee_paid);
14702        assert_eq!(state.get_balance(&bob).unwrap_or(0), 0);
14703    }
14704
14705    #[test]
14706    fn test_restricted_governance_authority_can_pay_fee_for_lift_remediation() {
14707        let (processor, state, alice_kp, alice, _treasury, genesis_hash) = setup();
14708        let validator = Pubkey([42u8; 32]);
14709        let fund = Account::licn_to_spores(1_000);
14710
14711        state
14712            .put_account(&alice, &Account::new(fund, alice))
14713            .unwrap();
14714        state.set_last_slot(0).unwrap();
14715        state.set_governance_authority(&alice).unwrap();
14716        state
14717            .set_governed_wallet_config(
14718                &alice,
14719                &crate::multisig::GovernedWalletConfig::new(1, vec![alice], "governance_authority"),
14720            )
14721            .unwrap();
14722
14723        let target_restriction_id = put_active_processor_test_restriction(
14724            &state,
14725            RestrictionTarget::Account(Pubkey([0xD8; 32])),
14726            RestrictionMode::OutgoingOnly,
14727        );
14728        let spendable = state.get_account(&alice).unwrap().unwrap().spendable;
14729        put_active_processor_test_restriction(
14730            &state,
14731            RestrictionTarget::Account(alice),
14732            RestrictionMode::OutgoingOnly,
14733        );
14734        put_active_processor_test_restriction(
14735            &state,
14736            RestrictionTarget::AccountAsset {
14737                account: alice,
14738                asset: NATIVE_LICN_ASSET_ID,
14739            },
14740            RestrictionMode::FrozenAmount { amount: spendable },
14741        );
14742
14743        let before_balance = state.get_balance(&alice).unwrap();
14744        let result = process_governance_proposal(
14745            &processor,
14746            &alice_kp,
14747            alice,
14748            alice,
14749            make_lift_restriction_action_data(
14750                target_restriction_id,
14751                RestrictionLiftReason::FalsePositive,
14752            ),
14753            genesis_hash,
14754            &validator,
14755        );
14756        assert!(
14757            result.success,
14758            "restricted governance authority should lift via governed flow: {:?}",
14759            result.error
14760        );
14761
14762        let lifted = state
14763            .get_restriction(target_restriction_id)
14764            .unwrap()
14765            .unwrap();
14766        assert_eq!(lifted.status, RestrictionStatus::Lifted);
14767        assert_eq!(lifted.lifted_by, Some(alice));
14768        let after_balance = state.get_balance(&alice).unwrap();
14769        assert_eq!(before_balance - after_balance, result.fee_paid);
14770    }
14771
14772    #[test]
14773    fn test_compute_budget_capped_succeeds() {
14774        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
14775        let bob = Pubkey([2u8; 32]);
14776        let validator = Pubkey([42u8; 32]);
14777
14778        let mut data = vec![0u8];
14779        data.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
14780        let ix = Instruction {
14781            program_id: SYSTEM_PROGRAM_ID,
14782            accounts: vec![alice, bob],
14783            data,
14784        };
14785        let mut message = crate::transaction::Message::new(vec![ix], genesis_hash);
14786        message.compute_budget = Some(crate::transaction::MAX_COMPUTE_BUDGET + 1);
14787        let mut tx = Transaction::new(message);
14788        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
14789
14790        let result = processor.process_transaction(&tx, &validator);
14791        // effective_compute_budget() caps at MAX, so this should succeed
14792        assert!(
14793            result.success,
14794            "Budget capped at MAX should succeed: {:?}",
14795            result.error
14796        );
14797    }
14798
14799    #[test]
14800    fn test_backward_compat_no_cu_fields() {
14801        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
14802        let bob = Pubkey([2u8; 32]);
14803        let validator = Pubkey([42u8; 32]);
14804
14805        let tx = make_transfer_tx(&alice_kp, alice, bob, 10, genesis_hash);
14806        assert!(tx.message.compute_budget.is_none());
14807        assert!(tx.message.compute_unit_price.is_none());
14808
14809        let result = processor.process_transaction(&tx, &validator);
14810        assert!(result.success);
14811        assert_eq!(result.fee_paid, BASE_FEE);
14812    }
14813
14814    #[test]
14815    fn test_simulation_fee_includes_priority() {
14816        let (processor, _state, alice_kp, alice, _treasury, genesis_hash) = setup();
14817        let bob = Pubkey([2u8; 32]);
14818
14819        let tx = make_transfer_tx_with_cu(
14820            &alice_kp,
14821            alice,
14822            bob,
14823            10,
14824            genesis_hash,
14825            Some(300_000),
14826            Some(500),
14827        );
14828        let sim = processor.simulate_transaction(&tx);
14829        assert!(sim.success, "Simulation should succeed: {:?}", sim.error);
14830        assert!(sim.compute_used > 0, "Should report compute used");
14831        let fee_config = FeeConfig::default_from_constants();
14832        let expected_fee = TxProcessor::compute_transaction_fee(&tx, &fee_config);
14833        assert_eq!(sim.fee, expected_fee);
14834    }
14835
14836    #[test]
14837    fn test_fee_free_txs_zero_base_with_priority() {
14838        let (_, _, alice_kp, alice, _, genesis_hash) = setup();
14839        let fee_config = FeeConfig::default_from_constants();
14840
14841        // Type 4 = Genesis transfer (fee-free)
14842        let mut data = vec![4u8];
14843        data.extend_from_slice(&Account::licn_to_spores(10).to_le_bytes());
14844        let ix = Instruction {
14845            program_id: SYSTEM_PROGRAM_ID,
14846            accounts: vec![alice, Pubkey([9u8; 32])],
14847            data,
14848        };
14849        let mut message = crate::transaction::Message::new(vec![ix], genesis_hash);
14850        message.compute_unit_price = Some(1000);
14851        let mut tx = Transaction::new(message);
14852        tx.signatures.push(alice_kp.sign(&tx.message.serialize()));
14853
14854        let base = TxProcessor::compute_base_fee(&tx, &fee_config);
14855        assert_eq!(base, 0, "Fee-free tx should have 0 base fee");
14856        let priority = TxProcessor::compute_priority_fee(&tx);
14857        assert_eq!(priority, 200); // 200K CU × 1000 μspores / 1M
14858    }
14859
14860    #[test]
14861    fn test_mempool_cu_price_ordering() {
14862        use crate::Mempool;
14863        let mut pool = Mempool::new(100, 300);
14864        let kp1 = Keypair::generate();
14865        let kp2 = Keypair::generate();
14866        let kp3 = Keypair::generate();
14867        let hash = Hash::hash(b"test");
14868
14869        let tx1 = {
14870            let ix = Instruction {
14871                program_id: SYSTEM_PROGRAM_ID,
14872                accounts: vec![kp1.pubkey()],
14873                data: vec![0u8],
14874            };
14875            let msg = crate::transaction::Message::new(vec![ix], hash);
14876            let mut tx = Transaction::new(msg);
14877            tx.signatures.push(kp1.sign(&tx.message.serialize()));
14878            tx
14879        };
14880
14881        let tx2 = {
14882            let ix = Instruction {
14883                program_id: SYSTEM_PROGRAM_ID,
14884                accounts: vec![kp2.pubkey()],
14885                data: vec![0u8],
14886            };
14887            let mut msg = crate::transaction::Message::new(vec![ix], hash);
14888            msg.compute_unit_price = Some(1000);
14889            let mut tx = Transaction::new(msg);
14890            tx.signatures.push(kp2.sign(&tx.message.serialize()));
14891            tx
14892        };
14893
14894        let tx3 = {
14895            let ix = Instruction {
14896                program_id: SYSTEM_PROGRAM_ID,
14897                accounts: vec![kp3.pubkey()],
14898                data: vec![0u8],
14899            };
14900            let mut msg = crate::transaction::Message::new(vec![ix], hash);
14901            msg.compute_unit_price = Some(5000);
14902            let mut tx = Transaction::new(msg);
14903            tx.signatures.push(kp3.sign(&tx.message.serialize()));
14904            tx
14905        };
14906
14907        let fee_config = FeeConfig::default_from_constants();
14908        let fee1 = TxProcessor::compute_transaction_fee(&tx1, &fee_config);
14909        let fee2 = TxProcessor::compute_transaction_fee(&tx2, &fee_config);
14910        let fee3 = TxProcessor::compute_transaction_fee(&tx3, &fee_config);
14911
14912        pool.add_transaction(tx1, fee1, 0).unwrap();
14913        pool.add_transaction(tx2, fee2, 0).unwrap();
14914        pool.add_transaction(tx3, fee3, 0).unwrap();
14915
14916        let top = pool.get_top_transactions(3);
14917        assert_eq!(top.len(), 3);
14918        assert_eq!(top[0].sender(), kp3.pubkey(), "Highest CU price first");
14919        assert_eq!(top[1].sender(), kp2.pubkey(), "Medium CU price second");
14920        assert_eq!(top[2].sender(), kp1.pubkey(), "No CU price last");
14921    }
14922
14923    #[test]
14924    fn test_message_serde_with_cu_fields() {
14925        let ix = Instruction {
14926            program_id: SYSTEM_PROGRAM_ID,
14927            accounts: vec![Pubkey([1u8; 32])],
14928            data: vec![0u8],
14929        };
14930        let mut msg = crate::transaction::Message::new(vec![ix], Hash::default());
14931        msg.compute_budget = Some(500_000);
14932        msg.compute_unit_price = Some(2000);
14933
14934        let serialized = msg.serialize();
14935        let deserialized: crate::transaction::Message = bincode::deserialize(&serialized).unwrap();
14936        assert_eq!(deserialized.compute_budget, Some(500_000));
14937        assert_eq!(deserialized.compute_unit_price, Some(2000));
14938        assert_eq!(deserialized.effective_compute_budget(), 500_000);
14939        assert_eq!(deserialized.effective_compute_unit_price(), 2000);
14940    }
14941
14942    #[test]
14943    fn test_message_serde_backward_compat() {
14944        let ix = Instruction {
14945            program_id: SYSTEM_PROGRAM_ID,
14946            accounts: vec![Pubkey([1u8; 32])],
14947            data: vec![0u8],
14948        };
14949        let msg = crate::transaction::Message::new(vec![ix], Hash::default());
14950        assert!(msg.compute_budget.is_none());
14951        assert!(msg.compute_unit_price.is_none());
14952        assert_eq!(
14953            msg.effective_compute_budget(),
14954            crate::transaction::DEFAULT_COMPUTE_BUDGET
14955        );
14956        assert_eq!(msg.effective_compute_unit_price(), 0);
14957    }
14958}