Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Designing the Contract: Multisignature Wallet

This chapter walks through the design of a real-world smart contract: a Multisignature Wallet for TON blockchain. We'll analyze the original Solidity implementation and understand its architecture before implementing it in Ursus.

What is a Multisignature Wallet?

A multisignature (multisig) wallet is a smart contract that requires multiple parties to approve a transaction before it can be executed. This provides enhanced security compared to single-signature wallets.

Key Concepts

Custodians: Authorized parties who can propose and approve transactions. Each custodian has a unique public key.

Confirmations: Number of custodian approvals required to execute a transaction. Configurable (e.g., 2-of-3, 3-of-5).

Transactions: Proposed transfers that wait for confirmations before execution.

Expiration: Transactions have a lifetime (1 hour) after which they are automatically removed.

Requirements Analysis

Functional Requirements

  1. Multi-party Authorization

    • Support up to 32 custodians
    • Configurable confirmation threshold (M-of-N)
    • Each custodian can propose transactions
    • Each custodian can confirm pending transactions
  2. Transaction Management

    • Create new transactions
    • Confirm pending transactions
    • Automatic execution when threshold reached
    • Automatic cleanup of expired transactions
  3. Security Features

    • Prevent double-confirmation by same custodian
    • Limit pending transactions per custodian (max 5)
    • Minimum transfer value check
    • Public key verification
  4. Special Features

    • Support for ECC tokens (TON-specific)
    • Direct send for single-custodian wallets
    • Configurable message flags
    • Payload support for complex messages

Security Requirements

  1. Access Control

    • Only custodians can propose transactions
    • Only custodians can confirm transactions
    • Constructor requires deployer's public key
  2. State Integrity

    • Prevent replay attacks (unique transaction IDs)
    • Prevent race conditions (atomic confirmations)
    • Prevent resource exhaustion (limits on pending transactions)
  3. Error Handling

    • Clear error codes for all failure cases
    • Require statements for all preconditions
    • Safe arithmetic (no overflows)

Performance Requirements

  1. Gas Optimization

    • Efficient storage layout
    • Batch cleanup of expired transactions
    • Inline helper functions
    • Minimal storage writes
  2. Scalability

    • Support up to 32 custodians
    • Handle up to 5 pending transactions per custodian
    • Configurable cleanup batch size

Architecture Design

Contract Structure

MultisigWallet
├── State Variables
│   ├── m_ownerKey              (deployer's public key)
│   ├── m_custodians            (mapping: pubkey → index)
│   ├── m_custodianCount        (total custodians)
│   ├── m_defaultRequiredConfirmations (M in M-of-N)
│   ├── m_transactions          (mapping: id → Transaction)
│   ├── m_requestsMask          (pending tx count per custodian)
│   ├── _min_value              (minimum transfer amount)
│   └── _max_cleanup_txns       (cleanup batch size)
│
├── Public Functions
│   ├── constructor             (initialize custodians)
│   ├── submitTransaction       (create new transaction)
│   ├── confirmTransaction      (approve pending transaction)
│   ├── sendTransaction         (direct send for 1 custodian)
│   ├── acceptTransfer          (receive incoming funds)
│   ├── setMinValue             (configure minimum)
│   └── setMaxCleanupTxns       (configure cleanup)
│
└── Internal Functions
    ├── _initialize             (setup custodians)
    ├── _findCustodian          (verify custodian)
    ├── _confirmTransaction     (process confirmation)
    ├── _removeExpiredTransactions (cleanup)
    ├── _generateId             (create unique ID)
    ├── _getExpirationBound     (calculate expiration)
    ├── _getSendFlags           (determine message flags)
    └── Bit manipulation helpers

Data Structures

Transaction Record

struct Transaction {
    uint64 id;                  // Unique identifier
    uint32 confirmationsMask;   // Bitfield of confirmations
    uint8 signsRequired;        // Threshold (M in M-of-N)
    uint8 signsReceived;        // Current confirmations
    uint256 creator;            // Proposer's public key
    uint8 index;                // Proposer's custodian index
    address dest;               // Destination address
    uint128 value;              // Transfer amount
    mapping(uint32 => varuint32) cc;  // ECC tokens
    uint16 sendFlags;           // Message flags
    TvmCell payload;            // Message body
    bool bounce;                // Bounce flag
}

Design Rationale:

  • id: Combines timestamp and logical time for uniqueness
  • confirmationsMask: Efficient storage using bitfield (32 bits for 32 custodians)
  • signsRequired/signsReceived: Track progress toward threshold
  • creator/index: Track who proposed the transaction
  • dest/value/payload: Standard transfer parameters
  • cc: TON-specific ECC token support
  • sendFlags/bounce: Message delivery options

Contract State

uint256 m_ownerKey;                     // Deployer's key
uint256 m_requestsMask;                 // Pending tx count (8 bits per custodian)
mapping(uint64 => Transaction) m_transactions;  // Active transactions
mapping(uint256 => uint8) m_custodians; // Custodian registry
uint8 m_custodianCount;                 // Total custodians
uint8 m_defaultRequiredConfirmations;   // Default threshold
uint128 _min_value;                     // Minimum transfer
uint _max_cleanup_txns;                 // Cleanup batch size

Design Rationale:

  • m_requestsMask: Compact storage - 8 bits per custodian in single uint256
  • m_transactions: Mapping for O(1) access by ID
  • m_custodians: Mapping for O(1) custodian lookup
  • Separate count and threshold for flexibility

Constants

uint8 constant MAX_QUEUED_REQUESTS = 5;      // Per-custodian limit
uint64 constant EXPIRATION_TIME = 3601;      // 1 hour + 1 second
uint8 constant MAX_CUSTODIAN_COUNT = 32;     // Maximum custodians
uint8 constant FLAG_PAY_FWD_FEE_FROM_BALANCE = 1;
uint8 constant FLAG_SEND_ALL_REMAINING = 128;

Design Rationale:

  • MAX_QUEUED_REQUESTS: Prevent spam from single custodian
  • EXPIRATION_TIME: Balance between usability and cleanup
  • MAX_CUSTODIAN_COUNT: Limited by confirmationsMask (32 bits)
  • Flags: TON-specific message delivery options

State Machine Design

Transaction Lifecycle

[Created] ──────────────────────────────────────────────┐
    │                                                    │
    │ submitTransaction()                               │
    ↓                                                    │
[Pending] ←──────────────────────────────┐              │
    │                                    │              │
    │ confirmTransaction()               │              │
    ↓                                    │              │
[Confirmed] (signsReceived++)            │              │
    │                                    │              │
    │ signsReceived < signsRequired      │              │
    ├────────────────────────────────────┘              │
    │                                                    │
    │ signsReceived >= signsRequired                    │
    ↓                                                    │
[Executed] ──> Transfer sent                            │
    │                                                    │
    ↓                                                    │
[Deleted]                                               │
                                                         │
    ↑                                                    │
    │ Time > EXPIRATION_TIME                            │
    └────────────────────────────────────────────────────┘

State Transitions

  1. Submit: Custodian creates new transaction

    • Preconditions: Valid custodian, value >= min_value, pending count < max
    • Effects: Create transaction, increment request mask
    • Special case: If M=1, execute immediately
  2. Confirm: Custodian approves pending transaction

    • Preconditions: Transaction exists, not expired, not already confirmed by this custodian
    • Effects: Set confirmation bit, increment signsReceived
    • Special case: If threshold reached, execute and delete
  3. Execute: Transaction reaches threshold

    • Preconditions: signsReceived >= signsRequired
    • Effects: Send transfer, decrement request mask, delete transaction
  4. Expire: Transaction exceeds lifetime

    • Preconditions: Current time - creation time > EXPIRATION_TIME
    • Effects: Delete transaction, decrement request mask

Workflow Examples

Example 1: 2-of-3 Multisig

Setup: 3 custodians (Alice, Bob, Carol), threshold = 2

Scenario: Alice wants to send 100 tokens to Dave

  1. Alice calls submitTransaction(Dave, 100, ...)

    • Transaction created with ID = 12345
    • confirmationsMask = 0b001 (Alice's bit set)
    • signsReceived = 1
    • Transaction stored in m_transactions[12345]
  2. Bob calls confirmTransaction(12345)

    • confirmationsMask = 0b011 (Alice + Bob)
    • signsReceived = 2
    • Threshold reached (2 >= 2)
    • Transfer executed to Dave
    • Transaction deleted
  3. Carol's confirmation not needed (threshold already reached)

Example 2: Single Custodian (Fast Path)

Setup: 1 custodian (Alice), threshold = 1

Scenario: Alice wants to send 100 tokens to Dave

  1. Alice calls submitTransaction(Dave, 100, ...)
    • Detects m_defaultRequiredConfirmations == 1
    • Executes transfer immediately
    • Returns transactionId = 0 (no transaction stored)
    • No confirmation needed

Example 3: Transaction Expiration

Setup: 3 custodians, threshold = 2

Scenario: Transaction expires before reaching threshold

  1. Alice submits transaction at time T

    • Transaction ID = (T << 32) | logicaltime
    • Stored in m_transactions
  2. Time passes: T + 3601 seconds

    • Transaction now expired
  3. Bob calls confirmTransaction() or submitTransaction()

    • _removeExpiredTransactions() called
    • Expired transaction detected and deleted
    • Bob's operation proceeds normally

Security Considerations

Attack Vectors and Mitigations

  1. Replay Attacks

    • Risk: Reuse old transaction signatures
    • Mitigation: Unique IDs based on timestamp + logical time
  2. Double Confirmation

    • Risk: Custodian confirms twice
    • Mitigation: Check confirmationsMask before setting bit
  3. Resource Exhaustion

    • Risk: Spam with many pending transactions
    • Mitigation: MAX_QUEUED_REQUESTS limit per custodian
  4. Unauthorized Access

    • Risk: Non-custodian creates transactions
    • Mitigation: _findCustodian() requires valid public key
  5. Integer Overflow

    • Risk: Arithmetic overflow in calculations
    • Mitigation: pragma ignoreIntOverflow + safe operations

Error Codes

100 - Message sender is not a custodian
101 - Zero owner (invalid public key)
102 - Transaction does not exist
103 - Already confirmed by this custodian
107 - Input value too low (< _min_value)
108 - Wallet should have only one custodian (for sendTransaction)
110 - Too many custodians (> MAX_CUSTODIAN_COUNT)
113 - Too many requests for one custodian (> MAX_QUEUED_REQUESTS)
117 - Invalid number of custodians (0 or > MAX)
123 - Need at least 1 required confirmation

Design Decisions and Trade-offs

1. Bitfield vs Array for Confirmations

Decision: Use uint32 bitfield for confirmationsMask

Rationale:

  • ✅ Compact storage (32 bits vs 32 bytes)
  • ✅ O(1) check and set operations
  • ✅ Atomic updates
  • ❌ Limited to 32 custodians
  • ❌ Requires bit manipulation

Alternative: Array of confirmed custodian indices

  • ✅ Unlimited custodians
  • ❌ Higher gas costs
  • ❌ More complex logic

2. Automatic Cleanup vs Manual Cleanup

Decision: Automatic cleanup on submit/confirm

Rationale:

  • ✅ No expired transactions accumulate
  • ✅ No separate cleanup transaction needed
  • ❌ Adds gas cost to submit/confirm
  • ❌ Unpredictable gas usage

Mitigation: Configurable _max_cleanup_txns to limit gas

3. Immediate Execution vs Always Queue

Decision: Immediate execution for M=1 case

Rationale:

  • ✅ Lower gas for single-custodian wallets
  • ✅ Simpler UX (no confirmation step)
  • ❌ Different code paths
  • ❌ More complex testing

4. Request Mask Encoding

Decision: 8 bits per custodian in single uint256

Rationale:

  • ✅ Compact storage (32 custodians in 256 bits)
  • ✅ Single storage slot
  • ❌ Complex bit manipulation
  • ❌ Limited to 255 pending per custodian

Next Steps

Now that we understand the contract design, we can:

  1. Specification - Write formal specifications for contract behavior
  2. Direct Proofs - Prove correctness properties
  3. Implementation - Implement in Ursus (when code generation is updated)

See Also