Skip to content

Maron09/Multi-Signature-Wallet-Smart-Contract-REST-API

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔐 Multi-Signature Wallet — Smart Contract + REST API

A production-grade multi-signature wallet built on Ethereum with a FastAPI backend. Requires multiple owner approvals before any transaction executes. Includes cryptographic signature verification so no owner's private key ever touches the server.


📋 Table of Contents


Overview

A multi-signature wallet requires M-of-N owner approvals before any ETH transfer executes. If the threshold is 2-of-3, any single owner acting alone cannot move funds — at least two must agree.

This project implements the full stack:

  • A Solidity smart contract that enforces approval logic on-chain
  • A FastAPI backend that exposes the contract as a REST API
  • Cryptographic signature verification that proves caller identity without the server ever holding private keys

This mirrors how real-world multi-sig tools like Gnosis Safe operate — used by DeFi protocols to secure billions of dollars in on-chain assets.


Architecture

Frontend (React / Mobile)
        ↕ HTTP + JSON
  FastAPI Backend
    ├── Signature Verification (eth_account)
    ├── ContractService (web3.py singleton)
    └── Pydantic request/response models
        ↕ web3.py
  Deployed Smart Contract
        ↕
  Ethereum Network (Ganache locally / Sepolia testnet)

Why This Architecture

The backend is a translation and verification layer — not a signer. It:

  • Converts between Solidity types and JSON
  • Verifies that API callers cryptographically own the addresses they claim
  • Relays verified transactions to the blockchain

Private keys never leave the owner's device.


Smart Contract

State Variables

Variable Type Description
owners address[] Array of all owner addresses
isOwner mapping(address => bool) O(1) ownership lookup
requiredApprovals uint256 Minimum approvals to execute
transactions Transaction[] All submitted transactions
approved mapping(uint256 => mapping(address => bool)) Per-transaction approval tracking

Transaction Struct

struct Transaction {
    address to;
    uint256 value;
    bool executed;
    uint256 approvalCount;
}

Functions

Function Access Description
submitTransaction(to, value) Owners only Propose a new ETH transfer
approveTransaction(txId) Owners only Vote to approve a transaction
executeTransaction(txId) Internal Fires automatically at threshold
revokeApproval(txId) Owners only Withdraw approval before execution
getTransaction(txId) Public view Read transaction details
getTransactionCount() Public view Total number of transactions
getOwners() Public view List all owner addresses

Modifiers

Modifier Protects Against
onlyOwner Non-owners calling restricted functions
txExists Referencing non-existent transaction IDs
notExecuted Re-approving or re-executing completed transactions
notApproved An owner approving the same transaction twice

Events

Event Parameters Emitted When
TransactionSubmitted txId, submitter, to, value New transaction proposed
TransactionApproved txId, owner Owner casts approval
ApprovalRevoked txId, owner Owner withdraws approval
TransactionExecuted txId Threshold reached, ETH sent
Deposit sender, value ETH received by contract

REST API

Models

Request models validate all incoming data via Pydantic. Every write operation requires a cryptographic signature proving the caller owns their claimed address.

Response models convert raw Solidity types (address, uint256, bool) into clean JSON for frontend consumption.

Endpoints

Method Path Description
GET /api/v1/wallet Contract address, owners, threshold, balance
GET /api/v1/balance Current ETH balance in the contract
GET /api/v1/transactions All submitted transactions
GET /api/v1/transactions/{tx_id} Single transaction details
POST /api/v1/wallet/deposit Send ETH to the contract
POST /api/v1/transactions/submit Propose a new transaction
POST /api/v1/transactions/approve Approve a transaction
POST /api/v1/transactions/revoke Revoke an approval

Interactive documentation available at /docs when the server is running.


Security Model

Two-Layer Defence

Layer 1 — API Level (signature verification)
  Every write request includes a signed message
  Backend recovers the signer address cryptographically
  If recovered address ≠ claimed address → request rejected

Layer 2 — Contract Level (onlyOwner modifier)
  Even if Layer 1 is bypassed, the contract checks ownership
  Non-owner transactions revert on-chain
  No state changes occur

Neither layer alone is sufficient. Together they form a complete security model.

Signature Verification Flow

1. Owner creates a message:
   "Approve transaction 0 at timestamp 1735000000"

2. Owner signs it with their private key (MetaMask / local wallet)
   → produces signature: 0x77d0f1...

3. Owner sends to API:
   { "tx_id": 0, "from_address": "0x66aB...",
     "signature": "0x77d0f1...", "message": "Approve transaction 0 at timestamp..." }

4. Backend recovers signer from signature:
   recovered = w3.eth.account.recover_message(message, signature)

5. Backend checks: recovered == from_address
   If not → "Signature mismatch" — request rejected

6. Backend checks timestamp age < 5 minutes
   If expired → "Signature expired" — request rejected

7. Transaction submitted to blockchain from verified address

Replay Attack Prevention

Messages include a Unix timestamp. The API rejects signatures older than 5 minutes — an attacker cannot reuse a captured signature after it expires.

# Message format
f"Approve transaction {tx_id} at timestamp {int(time.time())}"

What the Backend Never Does

  • Stores private keys
  • Signs transactions on behalf of owners
  • Trusts from_address without cryptographic proof

Project Structure

multisig-wallet/                  # Brownie smart contract project
├── contracts/
│   └── MultiSigWallet.sol        # Smart contract
├── tests/
│   └── test_multisig.py          # 21 tests
├── scripts/
│   └── deploy.py                 # Brownie deployment script
└── build/
    └── contracts/
        └── MultiSigWallet.json   # Compiled ABI + bytecode

multisig-api/                     # FastAPI backend
├── app/
│   ├── __init__.py
│   ├── main.py                   # FastAPI app entry point
│   ├── contract_service.py       # web3.py blockchain interface
│   ├── models.py                 # Pydantic request/response models
│   └── routes.py                 # API endpoint definitions
├── deploy_web3.py                # web3.py deployment script
├── test_signing.py               # Local signature testing utility
├── requirements.txt
└── .env                          # Environment variables (never commit)

Getting Started

Prerequisites

  • Python 3.11+
  • Node.js 16+
  • Ganache Desktop App
  • pip

1. Clone the Repository

git clone https://github.com/YOUR_USERNAME/multisig-wallet.git

2. Set Up the Smart Contract Project

cd multisig-wallet
python -m venv env
source env/bin/activate
pip install eth-brownie
brownie compile

3. Set Up Ganache

  • Open the Ganache desktop app
  • Create a new workspace named multisig-dev
  • Set mnemonic to: brownie
  • Set port to: 7545
  • Save and start the workspace

4. Deploy the Contract

cd multisig-api
source api-env/bin/activate  # or create: python -m venv api-env
pip install -r requirements.txt
python deploy_web3.py

Copy the deployed contract address from the output.

5. Configure Environment

Create multisig-api/.env:

GANACHE_URL=http://127.0.0.1:7545
CONTRACT_ADDRESS=<address from deploy output>
ABI_PATH=../multisig-wallet/build/contracts/MultiSigWallet.json

6. Start the API

cd multisig-api
source api-env/bin/activate
uvicorn app.main:app --reload

API running at: http://127.0.0.1:8000 Interactive docs at: http://127.0.0.1:8000/docs


API Endpoints

Get Wallet Info

curl http://127.0.0.1:8000/api/v1/wallet
{
  "contract_address": "0x3194...",
  "owners": ["0x66aB...", "0x33A4...", "0x0063..."],
  "required_approvals": 2,
  "balance_in_eth": 2.0
}

Submit a Transaction

curl -X POST http://127.0.0.1:8000/api/v1/transactions/submit \
  -H "Content-Type: application/json" \
  -d '{
    "to": "0x21b4...",
    "value_in_eth": 0.5,
    "from_address": "0x66aB...",
    "signature": "0x...",
    "message": "Submit transaction to 0x21b4 at timestamp 1735000000"
  }'

Approve a Transaction

curl -X POST http://127.0.0.1:8000/api/v1/transactions/approve \
  -H "Content-Type: application/json" \
  -d '{
    "tx_id": 0,
    "from_address": "0x66aB...",
    "signature": "0x...",
    "message": "Approve transaction 0 at timestamp 1735000000"
  }'

End-to-End Flow

1. Deploy contract with 3 owners, threshold of 2
2. Deposit ETH into the contract
3. Owner 1 signs a message and submits a transaction via API
4. Owner 1 signs a message and approves via API
5. Owner 2 signs a message and approves via API
   → Threshold reached → executeTransaction fires automatically
   → ETH transferred to recipient on-chain
6. GET /transactions/0 shows executed: true

Key Concepts

Why Two Data Structures for Owners

address[] public owners;                    // for iteration
mapping(address => bool) public isOwner;    // for O(1) lookup

The array lets you list all owners. The mapping lets you check ownership instantly without looping. Both are needed — they solve different problems.

Checks-Effects-Interactions Pattern

Every state-changing function follows this order:

// 1. CHECKS — validate everything first
require(isOwner[msg.sender], "Not an owner");
require(!approved[txId][msg.sender], "Already approved");

// 2. EFFECTS — update state
approved[txId][msg.sender] = true;
transactions[txId].approvalCount += 1;

// 3. INTERACTIONS — emit events, external calls
emit TransactionApproved(txId, msg.sender);

Setting executed = true before sending ETH in executeTransaction prevents reentrancy attacks.

Singleton ContractService

One shared web3.py connection is created at API startup and reused for every request — avoiding the overhead of opening a new blockchain connection per request. Same principle as database connection pooling.


Running the Smart Contract Tests

cd multisig-wallet
source env/bin/activate
brownie test -v

Test Coverage — 21 Tests

Covers deployment validation, ETH deposits, transaction submission, approval flow, threshold execution, ETH transfer confirmation, approval revocation, and all rejection cases including duplicate owners, invalid thresholds, double approvals, and non-owner access.


Roadmap

  • JWT authentication layer for session management
  • WebSocket support for real-time transaction status updates
  • Deploy to Sepolia testnet with Infura/Alchemy
  • Frontend React interface with MetaMask integration
  • Transaction history with event log indexing
  • Email/push notifications when threshold is reached
  • Support for ERC20 token transfers, not just ETH

Tech Stack

Smart Contract

  • Solidity ^0.8.0
  • Brownie 1.21.0
  • pytest

Backend

  • FastAPI
  • web3.py
  • Pydantic
  • uvicorn
  • eth-account
  • python-dotenv

Local Blockchain

  • Ganache Desktop

Security Notes

Never commit your .env file. Never share private keys. Always verify signatures server-side before submitting transactions. The .gitignore for this project excludes .env, api-env/, build/, __pycache__/, and *.pyc.


License

MIT

About

A production-grade multi-signature wallet built on Ethereum with a FastAPI backend. Requires multiple owner approvals before any transaction executes. Includes cryptographic signature verification so no owner's private key ever touches the server.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors