Skip to Content

⚠️ Disclaimer: This document is provided as a smart contract coding example only. The code has NOT been professionally audited. Use at your own risk - you bear full responsibility for any losses if deployed to production.

Multisig Wallet Contract

A multi-signature wallet that requires multiple owners to approve transactions before execution, providing enhanced security for shared funds.

Features

  • Multiple Owners: Configure 2-10 wallet owners
  • Threshold Approvals: Require N-of-M signatures
  • Transaction Proposals: Any owner can propose transactions
  • Approval Tracking: Track who approved each transaction
  • Execution Control: Execute only when threshold is met
  • Cancellation: Cancel pending transactions

Instruction Format

All instructions use format: [opcode:1][params:N]

OpcodeNameParameters
0x00Init[threshold:4][owner_count:4][owners:32*N]
0x01Propose[caller:32][to:32][value:8]
0x02Approve[caller:32][tx_id:8]
0x03Execute[caller:32][tx_id:8]
0x04Deposit[amount:8]
0x05Cancel[caller:32][tx_id:8]

Storage Layout

// Global state const KEY_THRESHOLD: &[u8] = b"thresh"; // Required approvals const KEY_OWNER_COUNT: &[u8] = b"oc"; // Number of owners const KEY_TX_COUNTER: &[u8] = b"txc"; // Transaction counter const KEY_BALANCE: &[u8] = b"bal"; // Wallet balance // Per-owner state const KEY_OWNER_PREFIX: &[u8] = b"own:"; // own:{address} -> is_owner // Per-transaction state const KEY_TX_PREFIX: &[u8] = b"tx:"; // tx:{id} -> tx data const KEY_APPROVAL_PREFIX: &[u8] = b"appr:"; // appr:{tx_id}{owner} -> approved

Transaction Status

const STATUS_PENDING: u8 = 0; // Awaiting approvals const STATUS_EXECUTED: u8 = 1; // Successfully executed const STATUS_CANCELLED: u8 = 2; // Cancelled by owner

Core Implementation

Initialize Wallet

fn init( threshold: u32, owner_count: u32, owners: &[[u8; 32]] ) -> entrypoint::Result<()> { // Threshold must be valid if threshold == 0 || threshold > owner_count { return Err(InvalidThreshold); } storage_write(KEY_THRESHOLD, &threshold.to_le_bytes())?; storage_write(KEY_OWNER_COUNT, &owner_count.to_le_bytes())?; storage_write(KEY_TX_COUNTER, &0u64.to_le_bytes())?; storage_write(KEY_BALANCE, &0u64.to_le_bytes())?; // Register all owners for owner in owners { let key = make_owner_key(owner); storage_write(&key, &[1u8])?; } log("Multisig wallet initialized"); Ok(()) }

Check Owner

fn is_owner(address: &Address) -> bool { let key = make_owner_key(address); let mut buffer = [0u8; 1]; let len = storage_read(&key, &mut buffer); len == 1 && buffer[0] != 0 }

Propose Transaction

fn propose_transaction( caller: &Address, to: &Address, value: u64 ) -> entrypoint::Result<()> { // Only owners can propose if !is_owner(caller) { return Err(OnlyOwner); } if *to == [0u8; 32] { return Err(InvalidRecipient); } let tx_id = read_u64(KEY_TX_COUNTER); // Transaction data: [to:32][value:8][status:1][approvals:4] = 45 bytes let mut tx_data = [0u8; 45]; tx_data[0..32].copy_from_slice(to); tx_data[32..40].copy_from_slice(&value.to_le_bytes()); tx_data[40] = STATUS_PENDING; // approvals start at 0 let key = make_tx_key(tx_id); storage_write(&key, &tx_data)?; // Increment transaction counter storage_write(KEY_TX_COUNTER, &(tx_id + 1).to_le_bytes())?; set_return_data(&tx_id.to_le_bytes()); log("Transaction proposed"); Ok(()) }

Approve Transaction

fn approve_transaction(caller: &Address, tx_id: TxId) -> entrypoint::Result<()> { if !is_owner(caller) { return Err(OnlyOwner); } // Read transaction let tx_key = make_tx_key(tx_id); let mut tx_data = [0u8; 45]; let len = storage_read(&tx_key, &mut tx_data); if len != 45 { return Err(TxNotFound); } // Check transaction is pending if tx_data[40] != STATUS_PENDING { return Err(TxNotPending); } // Check if already approved by this owner let approval_key = make_approval_key(tx_id, caller); let mut approval_buffer = [0u8; 1]; if storage_read(&approval_key, &mut approval_buffer) > 0 && approval_buffer[0] != 0 { return Err(AlreadyApproved); } // Record approval storage_write(&approval_key, &[1u8])?; // Increment approval count let approvals = u32::from_le_bytes(tx_data[41..45].try_into().unwrap()); tx_data[41..45].copy_from_slice(&(approvals + 1).to_le_bytes()); storage_write(&tx_key, &tx_data)?; log("Transaction approved"); Ok(()) }

Execute Transaction

fn execute_transaction(caller: &Address, tx_id: TxId) -> entrypoint::Result<()> { if !is_owner(caller) { return Err(OnlyOwner); } // Read transaction let tx_key = make_tx_key(tx_id); let mut tx_data = [0u8; 45]; let len = storage_read(&tx_key, &mut tx_data); if len != 45 { return Err(TxNotFound); } if tx_data[40] != STATUS_PENDING { return Err(TxNotPending); } // Check threshold is met let approvals = u32::from_le_bytes(tx_data[41..45].try_into().unwrap()); let threshold = read_u32(KEY_THRESHOLD); if approvals < threshold { return Err(InsufficientApprovals); } // Check balance let value = u64::from_le_bytes(tx_data[32..40].try_into().unwrap()); let balance = read_u64(KEY_BALANCE); if balance < value { return Err(InsufficientBalance); } // Deduct balance storage_write(KEY_BALANCE, &(balance - value).to_le_bytes())?; // Mark as executed tx_data[40] = STATUS_EXECUTED; storage_write(&tx_key, &tx_data)?; log("Transaction executed"); Ok(()) }

Deposit Funds

fn deposit(amount: u64) -> entrypoint::Result<()> { let balance = read_u64(KEY_BALANCE); storage_write(KEY_BALANCE, &(balance + amount).to_le_bytes())?; log("Deposit received"); Ok(()) }

Cancel Transaction

fn cancel_transaction(caller: &Address, tx_id: TxId) -> entrypoint::Result<()> { if !is_owner(caller) { return Err(OnlyOwner); } let tx_key = make_tx_key(tx_id); let mut tx_data = [0u8; 45]; let len = storage_read(&tx_key, &mut tx_data); if len != 45 { return Err(TxNotFound); } if tx_data[40] != STATUS_PENDING { return Err(TxNotPending); } tx_data[40] = STATUS_CANCELLED; storage_write(&tx_key, &tx_data)?; log("Transaction cancelled"); Ok(()) }

Error Codes

CodeNameDescription
1501OnlyOwnerCaller is not an owner
1502TxNotFoundTransaction ID doesn’t exist
1503TxNotPendingTransaction not in pending state
1504AlreadyApprovedOwner already approved this tx
1505NotApprovedOwner hasn’t approved this tx
1506InsufficientApprovalsNot enough approvals
1507InsufficientBalanceWallet balance too low
1508InvalidRecipientInvalid recipient address
1509StorageErrorStorage operation failed
1510InvalidInputInvalid instruction input
1511InvalidThresholdThreshold is invalid

Usage Example

// 1. Initialize 2-of-3 multisig wallet let init_data = [0u8; 105]; // 1 + 4 + 4 + 96 init_data[0] = 0; // Opcode: Init init_data[1..5].copy_from_slice(&2u32.to_le_bytes()); // threshold = 2 init_data[5..9].copy_from_slice(&3u32.to_le_bytes()); // 3 owners init_data[9..41].copy_from_slice(&owner1_address); init_data[41..73].copy_from_slice(&owner2_address); init_data[73..105].copy_from_slice(&owner3_address); // 2. Deposit funds to wallet let deposit_data = [0u8; 9]; deposit_data[0] = 4; // Opcode: Deposit deposit_data[1..9].copy_from_slice(&1000u64.to_le_bytes()); // 3. Owner 1 proposes a transaction let propose_data = [0u8; 73]; propose_data[0] = 1; // Opcode: Propose propose_data[1..33].copy_from_slice(&owner1_address); propose_data[33..65].copy_from_slice(&recipient_address); propose_data[65..73].copy_from_slice(&500u64.to_le_bytes()); // 4. Owner 1 approves (returns tx_id) let approve1_data = [0u8; 41]; approve1_data[0] = 2; // Opcode: Approve approve1_data[1..33].copy_from_slice(&owner1_address); approve1_data[33..41].copy_from_slice(&0u64.to_le_bytes()); // tx_id = 0 // 5. Owner 2 approves let approve2_data = [0u8; 41]; approve2_data[0] = 2; approve2_data[1..33].copy_from_slice(&owner2_address); approve2_data[33..41].copy_from_slice(&0u64.to_le_bytes()); // 6. Any owner executes (threshold of 2 met) let execute_data = [0u8; 41]; execute_data[0] = 3; // Opcode: Execute execute_data[1..33].copy_from_slice(&owner1_address); execute_data[33..41].copy_from_slice(&0u64.to_le_bytes());

Common Configurations

TypeThresholdOwnersUse Case
2-of-323Small team, personal
3-of-535Project treasury
4-of-747DAO treasury
2-of-222Joint accounts

Security Considerations

  1. Owner Management: Consider adding/removing owners functionality
  2. Timelock: Add delay before execution for high-value transactions
  3. Daily Limits: Implement spending limits without full approval
  4. Key Recovery: Plan for lost keys scenarios
  5. Nonce: Add nonces to prevent replay attacks
Last updated on