⚠️ 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]
| Opcode | Name | Parameters |
|---|---|---|
| 0x00 | Init | [threshold:4][owner_count:4][owners:32*N] |
| 0x01 | Propose | [caller:32][to:32][value:8] |
| 0x02 | Approve | [caller:32][tx_id:8] |
| 0x03 | Execute | [caller:32][tx_id:8] |
| 0x04 | Deposit | [amount:8] |
| 0x05 | Cancel | [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} -> approvedTransaction Status
const STATUS_PENDING: u8 = 0; // Awaiting approvals
const STATUS_EXECUTED: u8 = 1; // Successfully executed
const STATUS_CANCELLED: u8 = 2; // Cancelled by ownerCore 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
| Code | Name | Description |
|---|---|---|
| 1501 | OnlyOwner | Caller is not an owner |
| 1502 | TxNotFound | Transaction ID doesn’t exist |
| 1503 | TxNotPending | Transaction not in pending state |
| 1504 | AlreadyApproved | Owner already approved this tx |
| 1505 | NotApproved | Owner hasn’t approved this tx |
| 1506 | InsufficientApprovals | Not enough approvals |
| 1507 | InsufficientBalance | Wallet balance too low |
| 1508 | InvalidRecipient | Invalid recipient address |
| 1509 | StorageError | Storage operation failed |
| 1510 | InvalidInput | Invalid instruction input |
| 1511 | InvalidThreshold | Threshold 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
| Type | Threshold | Owners | Use Case |
|---|---|---|---|
| 2-of-3 | 2 | 3 | Small team, personal |
| 3-of-5 | 3 | 5 | Project treasury |
| 4-of-7 | 4 | 7 | DAO treasury |
| 2-of-2 | 2 | 2 | Joint accounts |
Security Considerations
- Owner Management: Consider adding/removing owners functionality
- Timelock: Add delay before execution for high-value transactions
- Daily Limits: Implement spending limits without full approval
- Key Recovery: Plan for lost keys scenarios
- Nonce: Add nonces to prevent replay attacks
Related Examples
- Governance - DAO governance
- Access Control - Role-based access
Last updated on