1use 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#[derive(Debug, Clone)]
49pub struct TxResult {
50 pub success: bool,
51 pub fee_paid: u64,
52 pub error: Option<String>,
53 pub compute_units_used: u64,
55 pub return_code: Option<i64>,
59 pub contract_logs: Vec<String>,
61 pub return_data: Vec<u8>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66enum TxProcessorMode {
67 Canonical,
68 Speculative,
69}
70
71pub struct SpeculativeBlockExecution {
73 pub results: Vec<TxResult>,
74 pub batch: StateBatch,
75}
76
77#[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#[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 pub return_code: Option<i64>,
98 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
150pub 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
158pub const CONTRACT_PROGRAM_ID: Pubkey = Pubkey([0xFFu8; 32]);
160
161pub const EVM_SENTINEL_BLOCKHASH: Hash = Hash([0xEE; 32]);
167
168pub const SLOTS_PER_MONTH: u64 = 216_000 * 30;
170
171pub const RENT_FREE_BYTES: u64 = 2048;
173
174pub const DORMANCY_THRESHOLD_EPOCHS: u64 = 2;
176
177const SECONDS_PER_DAY: u64 = 86_400;
178
179pub const MAX_TX_AGE_BLOCKS: u64 = 300;
182pub const BASE_FEE: u64 = 1_000_000;
186
187pub const CONTRACT_DEPLOY_FEE: u64 = 25_000_000_000;
190
191pub const CONTRACT_UPGRADE_FEE: u64 = 10_000_000_000;
194
195pub const NFT_MINT_FEE: u64 = 500_000_000;
198
199pub const NFT_COLLECTION_FEE: u64 = 1_000_000_000_000;
202
203pub const NONCE_ACCOUNT_MIN_BALANCE: u64 = 10_000_000;
206
207pub const NONCE_ACCOUNT_MARKER: u8 = 0xDA;
209
210pub const CONFLICT_KEY_STAKE_POOL: Pubkey = Pubkey([0xFE; 32]);
218pub const CONFLICT_KEY_MOSSSTAKE_POOL: Pubkey = Pubkey([0xFD; 32]);
220pub const CONFLICT_KEY_GOVERNED_PROPOSALS: Pubkey = Pubkey([0xFC; 32]);
222pub const CONFLICT_KEY_GOVERNANCE_PROPOSALS: Pubkey = Pubkey([0xFB; 32]);
224pub 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
241pub const GOV_PARAM_BASE_FEE: u8 = 0;
243pub const GOV_PARAM_FEE_BURN_PERCENT: u8 = 1;
245pub const GOV_PARAM_FEE_PRODUCER_PERCENT: u8 = 2;
247pub const GOV_PARAM_FEE_VOTERS_PERCENT: u8 = 3;
249pub const GOV_PARAM_FEE_TREASURY_PERCENT: u8 = 4;
251pub const GOV_PARAM_FEE_COMMUNITY_PERCENT: u8 = 5;
253pub const GOV_PARAM_MIN_VALIDATOR_STAKE: u8 = 6;
255pub 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
283pub const ORACLE_ASSET_MIN_LEN: usize = 1;
285pub const ORACLE_ASSET_MAX_LEN: usize = 16;
287pub const ORACLE_STALENESS_SLOTS: u64 = 9_000;
289
290pub 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
322pub 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#[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#[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
355pub 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#[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
389pub 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
507pub 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 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 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 let alice_account = Account::new(1000, alice);
643 state.put_account(&alice, &alice_account).unwrap();
644
645 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(
979 final_balance,
980 initial_balance - deposit_amount - result.fee_paid
981 );
982
983 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 #[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 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 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 let after = state.get_account(&alice).unwrap().unwrap().spendable;
1582 let charged = before - after;
1583 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 #[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 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 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 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]
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 let code = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1849
1850 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 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 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 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 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]
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 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 let final_balance = state.get_balance(&alice).unwrap();
2001 let fee_kept = initial_balance - final_balance;
2002 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 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 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 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 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 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 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 let treasury_kp = Keypair::from_seed(&[3u8; 32]);
2193 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 let (processor, state, _alice_kp, alice, treasury, _genesis_hash) = setup();
2228
2229 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 let large_fee: u64 = 1_000_000_000_000_000_000; 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 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 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 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 #[test]
2304 fn test_parallel_disjoint_txs_succeed() {
2305 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 let alice_account = Account::new(500, alice);
2313 state.put_account(&alice, &alice_account).unwrap();
2314
2315 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 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 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 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 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 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 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 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 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 #[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 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]; 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 #[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 let ix = crate::transaction::Instruction {
2641 program_id: crate::evm::EVM_PROGRAM_ID,
2642 accounts: vec![alice],
2643 data: vec![0xDE, 0xAD], };
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 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 #[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 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); 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 proc_a.charge_fee_direct(&payer_a, fee).unwrap();
2704
2705 proc_b.charge_fee_direct(&payer_b, fee).unwrap();
2707
2708 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 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 #[test]
2724 fn test_fee_split_capped_no_spore_creation() {
2725 let (processor, state, _alice_kp, _alice, treasury, _genesis_hash) = setup();
2726
2727 let payer = Pubkey([99u8; 32]);
2729 state.put_account(&payer, &Account::new(10, payer)).unwrap();
2730
2731 let fee = Account::licn_to_spores(1); 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 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 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 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 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 #[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 state
2864 .put_account(&treasury, &Account::new(1_000_000, treasury))
2865 .unwrap();
2866
2867 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]; 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]; 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 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 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 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 state
3003 .put_account(&alice, &Account::new(10_000, alice))
3004 .unwrap();
3005
3006 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 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 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], 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 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 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_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 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_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 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_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 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 fn setup_validator_in_pool(state: &StateStore, validator: Pubkey) {
3236 let mut pool = state.get_stake_pool().unwrap_or_default();
3237 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 setup_validator_in_pool(&state, validator);
3353
3354 state
3356 .put_account(&alice, &Account::new(100_000, alice))
3357 .unwrap();
3358
3359 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 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 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]); 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 state
3575 .put_account(&alice, &Account::new(100_000, alice))
3576 .unwrap();
3577
3578 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 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 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 state
3673 .put_account(&alice, &Account::new(100_000, alice))
3674 .unwrap();
3675
3676 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 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 state
3721 .put_account(&alice, &Account::new(200_000, alice))
3722 .unwrap();
3723
3724 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 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 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 #[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 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 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 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 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 #[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 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]; 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 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 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]; 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 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 fn deploy_fake_contract(state: &StateStore, owner: Pubkey, contract_id: Pubkey) {
4152 let contract = crate::ContractAccount {
4153 code: vec![0x00, 0x61, 0x73, 0x6d], 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 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 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 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 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 #[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 #[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); 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 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 #[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], };
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 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 #[test]
4680 fn test_ecosystem_grant_requires_multisig() {
4681 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 let eco_acct = Account::new(Account::licn_to_spores(1000), Pubkey([0u8; 32]));
4689 state.put_account(&eco, &eco_acct).unwrap();
4690
4691 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 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 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 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 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 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 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 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 let mut approve_data = vec![22u8];
5074 approve_data.extend_from_slice(&1u64.to_le_bytes()); 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 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 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 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 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 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 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 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 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 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 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 #[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 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()); data.extend_from_slice(&[0xAA; 32]); data.extend_from_slice(&[0xBB; 128]); 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![], data,
5565 };
5566 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 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]); data.extend_from_slice(&[0xFF; 7]); 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 let amount = 500_000_000u64; 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 let zk_proof = Prover::new().prove_shield(circuit).unwrap();
5657
5658 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(¬e_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 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 let alice_balance_after = state.get_balance(&alice).unwrap();
5693 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 let pool = state.get_shielded_pool_state().unwrap();
5706 assert_eq!(pool.commitment_count, 1);
5707 assert_eq!(pool.total_shielded, amount);
5708
5709 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 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 #[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]); 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 let amount = 100u64;
6129 let nullifier = [0x11u8; 32];
6130 let merkle_root = [0u8; 32];
6131
6132 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]); 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 #[test]
6190 fn test_graduated_rent_below_free_tier() {
6191 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 assert_eq!(compute_graduated_rent(3 * 1024, 100), 100);
6201 assert_eq!(compute_graduated_rent(10 * 1024, 100), 800);
6203 }
6204
6205 #[test]
6206 fn test_graduated_rent_tier2() {
6207 assert_eq!(compute_graduated_rent(11 * 1024, 100), 800 + 200);
6209 assert_eq!(compute_graduated_rent(50 * 1024, 100), 800 + 8000);
6211 assert_eq!(compute_graduated_rent(100 * 1024, 100), 800 + 18000);
6213 }
6214
6215 #[test]
6216 fn test_graduated_rent_tier3() {
6217 assert_eq!(compute_graduated_rent(101 * 1024, 100), 800 + 18000 + 400);
6219 assert_eq!(compute_graduated_rent(200 * 1024, 100), 800 + 18000 + 40000);
6221 }
6222
6223 #[test]
6224 fn test_graduated_rent_partial_kb() {
6225 assert_eq!(compute_graduated_rent(2049, 100), 100);
6227 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 fn make_nonce_init_ix(funder: Pubkey, nonce_pk: Pubkey, authority: Pubkey) -> Instruction {
6240 let mut data = vec![28u8, 0u8]; 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 fn make_nonce_advance_ix(authority: Pubkey, nonce_pk: Pubkey) -> Instruction {
6251 let data = vec![28u8, 1u8]; 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(state.get_balance(&bob).unwrap(), Account::licn_to_spores(1));
6550
6551 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_test_nonce(
6566 &processor,
6567 &alice_kp,
6568 alice,
6569 nonce_pk,
6570 alice,
6571 genesis_hash,
6572 &validator,
6573 );
6574
6575 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 let bob_balance = state.get_balance(&bob).unwrap();
6583 assert_eq!(bob_balance, NONCE_ACCOUNT_MIN_BALANCE);
6584
6585 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 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 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 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 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 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 assert!(TxProcessor::decode_nonce_state(&[]).is_err());
6819 assert!(TxProcessor::decode_nonce_state(&[0x00, 0x01]).is_err());
6821 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], };
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 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 state.set_governance_authority(&alice).unwrap();
6864
6865 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 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 let applied = state.apply_pending_governance_changes().unwrap();
6881 assert_eq!(applied, 1);
6882
6883 let fee_config = state.get_fee_config().unwrap();
6885 assert_eq!(fee_config.base_fee, new_base_fee);
6886
6887 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 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 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 let new_stake = 100_000_000_000u64; 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 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 let gov_auth = Pubkey([77u8; 32]);
6974 state.set_governance_authority(&gov_auth).unwrap();
6975
6976 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 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 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 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 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 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 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 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 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 #[test]
7520 fn test_cu_lookup_transfer() {
7521 assert_eq!(compute_units_for_system_ix(0), CU_TRANSFER);
7522 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], };
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]), 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 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 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 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 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 setup_active_validator(&state, &alice, MIN_VALIDATOR_STAKE);
7668
7669 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 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 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 let mut data = vec![30u8, 0u8]; 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 let long_asset = "ABCDEFGHIJKLMNOPQ"; 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 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 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 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 let block_producer = Pubkey([42u8; 32]);
7847
7848 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn valid_wasm_code(tag: u8) -> Vec<u8> {
8219 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 let contract_addr = deploy_test_contract(
11349 &processor,
11350 &state,
11351 &alice_kp,
11352 alice,
11353 genesis_hash,
11354 &validator,
11355 );
11356
11357 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 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 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 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 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 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 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 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 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 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 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 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 let gov_kp = crate::Keypair::generate();
14225 let gov = gov_kp.pubkey();
14226 state.set_governance_authority(&gov).unwrap();
14227 let gov_acct = crate::Account::new(10, gov);
14229 state.put_account(&gov, &gov_acct).unwrap();
14230
14231 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 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 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 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 let gov_kp = crate::Keypair::generate();
14296 let gov = gov_kp.pubkey();
14297 state.set_governance_authority(&gov).unwrap();
14298
14299 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 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 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 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 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 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 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 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 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 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 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 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 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 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); }
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}