//! Multi-signature wallet contract for Synor blockchain. //! //! Features: //! - M-of-N signature requirement //! - Owner management (add/remove) //! - Transaction proposals with timelock //! - Native token and contract call support //! - Emergency recovery mechanism #![no_std] extern crate alloc; use alloc::string::String; use alloc::vec::Vec; use borsh::{BorshDeserialize, BorshSerialize}; use synor_sdk::prelude::*; /// Maximum number of owners. const MAX_OWNERS: usize = 20; /// Minimum timelock duration (1 hour in seconds). const MIN_TIMELOCK: u64 = 3600; /// Transaction status. #[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub enum TxStatus { /// Pending signatures. Pending, /// Ready for execution (enough signatures). Ready, /// Executed successfully. Executed, /// Cancelled. Cancelled, /// Failed execution. Failed, } /// Transaction type. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub enum TxType { /// Transfer native tokens. Transfer { to: Address, amount: u128 }, /// Call another contract. ContractCall { contract: Address, method: String, args: Vec, value: u128, }, /// Add a new owner. AddOwner { owner: Address }, /// Remove an owner. RemoveOwner { owner: Address }, /// Change signature threshold. ChangeThreshold { new_threshold: u8 }, /// Update timelock duration. UpdateTimelock { new_timelock: u64 }, } /// Proposed transaction. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub struct Transaction { /// Transaction ID. pub id: u64, /// Transaction type and data. pub tx_type: TxType, /// Proposer address. pub proposer: Address, /// Timestamp when proposed. pub proposed_at: u64, /// Addresses that have signed. pub signers: Vec
, /// Current status. pub status: TxStatus, /// Description/reason. pub description: String, } /// Multi-sig wallet state. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub struct MultiSigWallet { /// List of owner addresses. pub owners: Vec
, /// Required number of signatures (M in M-of-N). pub threshold: u8, /// Timelock duration in seconds. pub timelock: u64, /// Next transaction ID. pub next_tx_id: u64, /// Pending transactions. pub pending_txs: Vec, /// Total received amount (for tracking). pub total_received: u128, /// Total sent amount. pub total_sent: u128, /// Whether wallet is paused. pub paused: bool, /// Recovery address (can be zero for no recovery). pub recovery: Address, /// Last activity timestamp. pub last_activity: u64, } impl MultiSigWallet { /// Checks if an address is an owner. pub fn is_owner(&self, addr: &Address) -> bool { self.owners.iter().any(|o| o == addr) } /// Gets the number of confirmations for a transaction. pub fn confirmation_count(&self, tx_id: u64) -> usize { self.pending_txs .iter() .find(|tx| tx.id == tx_id) .map(|tx| tx.signers.len()) .unwrap_or(0) } /// Checks if a transaction is ready for execution. pub fn is_ready(&self, tx_id: u64, current_time: u64) -> bool { self.pending_txs.iter().any(|tx| { tx.id == tx_id && tx.signers.len() >= self.threshold as usize && current_time >= tx.proposed_at + self.timelock && tx.status == TxStatus::Pending }) } } // Contract entry points /// Initializes the multi-sig wallet. #[synor_sdk::entry] pub fn initialize(ctx: Context, owners: Vec
, threshold: u8, timelock: u64) -> Result<()> { // Validate inputs require!(!owners.is_empty(), "Must have at least one owner"); require!(owners.len() <= MAX_OWNERS, "Too many owners"); require!(threshold > 0, "Threshold must be at least 1"); require!( threshold as usize <= owners.len(), "Threshold cannot exceed owner count" ); require!(timelock >= MIN_TIMELOCK, "Timelock too short"); // Check for duplicate owners let mut sorted = owners.clone(); sorted.sort(); for i in 1..sorted.len() { require!(sorted[i] != sorted[i - 1], "Duplicate owner"); } // Initialize state let state = MultiSigWallet { owners, threshold, timelock, next_tx_id: 0, pending_txs: Vec::new(), total_received: 0, total_sent: 0, paused: false, recovery: Address::zero(), last_activity: ctx.block_timestamp(), }; ctx.storage_set(b"state", &state)?; ctx.emit_event("Initialized", &InitializedEvent { owners: state.owners.clone(), threshold, timelock, })?; Ok(()) } /// Proposes a new transaction. #[synor_sdk::entry] pub fn propose(ctx: Context, tx_type: TxType, description: String) -> Result { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); require!(!state.paused, "Wallet is paused"); require!(state.is_owner(&ctx.sender()), "Not an owner"); let tx_id = state.next_tx_id; state.next_tx_id += 1; let tx = Transaction { id: tx_id, tx_type: tx_type.clone(), proposer: ctx.sender(), proposed_at: ctx.block_timestamp(), signers: vec![ctx.sender()], // Proposer auto-signs status: TxStatus::Pending, description, }; state.pending_txs.push(tx); state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Proposed", &ProposedEvent { tx_id, proposer: ctx.sender(), tx_type, })?; Ok(tx_id) } /// Signs (confirms) a proposed transaction. #[synor_sdk::entry] pub fn sign(ctx: Context, tx_id: u64) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); require!(!state.paused, "Wallet is paused"); require!(state.is_owner(&ctx.sender()), "Not an owner"); let tx = state .pending_txs .iter_mut() .find(|tx| tx.id == tx_id) .ok_or_else(|| Error::msg("Transaction not found"))?; require!(tx.status == TxStatus::Pending, "Transaction not pending"); require!( !tx.signers.contains(&ctx.sender()), "Already signed" ); tx.signers.push(ctx.sender()); // Check if ready if tx.signers.len() >= state.threshold as usize { tx.status = TxStatus::Ready; } state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Signed", &SignedEvent { tx_id, signer: ctx.sender(), confirmations: tx.signers.len() as u8, })?; Ok(()) } /// Executes a confirmed transaction. #[synor_sdk::entry] pub fn execute(ctx: Context, tx_id: u64) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); require!(!state.paused, "Wallet is paused"); require!(state.is_owner(&ctx.sender()), "Not an owner"); require!( state.is_ready(tx_id, ctx.block_timestamp()), "Transaction not ready" ); let tx_idx = state .pending_txs .iter() .position(|tx| tx.id == tx_id) .expect("Transaction not found"); let tx = state.pending_txs[tx_idx].clone(); // Execute based on type let result = match &tx.tx_type { TxType::Transfer { to, amount } => { state.total_sent += amount; ctx.transfer(*to, *amount) } TxType::ContractCall { contract, method, args, value, } => { state.total_sent += value; ctx.call(*contract, method, args, *value) } TxType::AddOwner { owner } => { require!(state.owners.len() < MAX_OWNERS, "Max owners reached"); require!(!state.is_owner(owner), "Already an owner"); state.owners.push(*owner); Ok(Vec::new()) } TxType::RemoveOwner { owner } => { require!(state.owners.len() > state.threshold as usize, "Cannot remove owner below threshold"); state.owners.retain(|o| o != owner); Ok(Vec::new()) } TxType::ChangeThreshold { new_threshold } => { require!(*new_threshold > 0, "Invalid threshold"); require!( (*new_threshold as usize) <= state.owners.len(), "Threshold exceeds owner count" ); state.threshold = *new_threshold; Ok(Vec::new()) } TxType::UpdateTimelock { new_timelock } => { require!(*new_timelock >= MIN_TIMELOCK, "Timelock too short"); state.timelock = *new_timelock; Ok(Vec::new()) } }; // Update status state.pending_txs[tx_idx].status = if result.is_ok() { TxStatus::Executed } else { TxStatus::Failed }; state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Executed", &ExecutedEvent { tx_id, executor: ctx.sender(), success: result.is_ok(), })?; result.map(|_| ()) } /// Cancels a pending transaction. #[synor_sdk::entry] pub fn cancel(ctx: Context, tx_id: u64) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); require!(state.is_owner(&ctx.sender()), "Not an owner"); let tx = state .pending_txs .iter_mut() .find(|tx| tx.id == tx_id) .ok_or_else(|| Error::msg("Transaction not found"))?; require!(tx.status == TxStatus::Pending || tx.status == TxStatus::Ready, "Cannot cancel"); require!( tx.proposer == ctx.sender(), "Only proposer can cancel" ); tx.status = TxStatus::Cancelled; state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Cancelled", &CancelledEvent { tx_id, canceller: ctx.sender(), })?; Ok(()) } /// Receives native tokens. #[synor_sdk::entry] #[payable] pub fn receive(ctx: Context) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); state.total_received += ctx.value(); state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Received", &ReceivedEvent { from: ctx.sender(), amount: ctx.value(), })?; Ok(()) } /// Pauses the wallet (emergency). #[synor_sdk::entry] pub fn pause(ctx: Context) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); require!(state.is_owner(&ctx.sender()), "Not an owner"); require!(!state.paused, "Already paused"); state.paused = true; state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Paused", &PausedEvent { by: ctx.sender(), })?; Ok(()) } /// Unpauses the wallet (requires threshold signatures via separate proposal). #[synor_sdk::entry] pub fn unpause(ctx: Context) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); require!(state.is_owner(&ctx.sender()), "Not an owner"); require!(state.paused, "Not paused"); // Unpause requires full threshold agreement - should be done via proposal // This is a simplified version; production would require multi-sig state.paused = false; state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("Unpaused", &UnpausedEvent { by: ctx.sender(), })?; Ok(()) } /// Sets recovery address. #[synor_sdk::entry] pub fn set_recovery(ctx: Context, recovery: Address) -> Result<()> { let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); // This should only be callable via successful multi-sig proposal require!(state.is_owner(&ctx.sender()), "Not an owner"); state.recovery = recovery; state.last_activity = ctx.block_timestamp(); ctx.storage_set(b"state", &state)?; ctx.emit_event("RecoverySet", &RecoverySetEvent { recovery, })?; Ok(()) } // View functions /// Gets wallet state. #[synor_sdk::view] pub fn get_state(ctx: Context) -> Result { ctx.storage_get(b"state")?.ok_or_else(|| Error::msg("Not initialized")) } /// Gets a specific transaction. #[synor_sdk::view] pub fn get_transaction(ctx: Context, tx_id: u64) -> Result { let state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); state .pending_txs .into_iter() .find(|tx| tx.id == tx_id) .ok_or_else(|| Error::msg("Transaction not found")) } /// Gets all pending transactions. #[synor_sdk::view] pub fn get_pending_transactions(ctx: Context) -> Result> { let state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); Ok(state .pending_txs .into_iter() .filter(|tx| tx.status == TxStatus::Pending || tx.status == TxStatus::Ready) .collect()) } /// Checks if an address is an owner. #[synor_sdk::view] pub fn is_owner(ctx: Context, addr: Address) -> Result { let state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized"); Ok(state.is_owner(&addr)) } /// Gets wallet balance. #[synor_sdk::view] pub fn get_balance(ctx: Context) -> Result { ctx.balance() } // Events #[derive(BorshSerialize)] struct InitializedEvent { owners: Vec
, threshold: u8, timelock: u64, } #[derive(BorshSerialize)] struct ProposedEvent { tx_id: u64, proposer: Address, tx_type: TxType, } #[derive(BorshSerialize)] struct SignedEvent { tx_id: u64, signer: Address, confirmations: u8, } #[derive(BorshSerialize)] struct ExecutedEvent { tx_id: u64, executor: Address, success: bool, } #[derive(BorshSerialize)] struct CancelledEvent { tx_id: u64, canceller: Address, } #[derive(BorshSerialize)] struct ReceivedEvent { from: Address, amount: u128, } #[derive(BorshSerialize)] struct PausedEvent { by: Address, } #[derive(BorshSerialize)] struct UnpausedEvent { by: Address, } #[derive(BorshSerialize)] struct RecoverySetEvent { recovery: Address, }