Secure Multi-Signature Wallet Solidity, JavaScript

👤 Sharing: AI
Okay, here's an example of a basic multi-signature wallet implemented in Solidity and a simple JavaScript interaction script.  I'll break down the code and explain the core concepts.

**Solidity (Smart Contract): `MultiSigWallet.sol`**

```solidity
pragma solidity ^0.8.0;

contract MultiSigWallet {
    address[] public owners;
    uint public numConfirmationsRequired;
    mapping(address => bool) public isOwner;
    mapping(uint => Transaction) public transactions;
    uint public transactionCount;
    mapping(uint => mapping(address => bool)) public confirmations;

    struct Transaction {
        address to;
        uint value;
        bytes data;
        bool executed;
        uint numConfirmations;
    }

    event Deposit(address indexed sender, uint value);
    event Submission(uint indexed transactionId);
    event Confirmation(address indexed owner, uint indexed transactionId);
    event Revocation(address indexed owner, uint indexed transactionId);
    event Execution(uint indexed transactionId);
    event ExecutionFailure(uint indexed transactionId);

    modifier onlyOwners() {
        require(isOwner[msg.sender], "Sender is not an owner");
        _;
    }

    modifier notExecuted(uint transactionId) {
        require(!transactions[transactionId].executed, "Transaction already executed");
        _;
    }

    modifier validRequirement(uint numOwners, uint _numConfirmationsRequired) {
        require(_numConfirmationsRequired > 0 && _numConfirmationsRequired <= numOwners, "Invalid number of required confirmations");
        _;
    }

    constructor(address[] memory _owners, uint _numConfirmationsRequired) validRequirement(_owners.length, _numConfirmationsRequired) {
        require(_owners.length > 0, "Owners required");

        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "Invalid owner address");
            require(!isOwner[owner], "Owner addresses must be unique");

            owners.push(owner);
            isOwner[owner] = true;
        }

        numConfirmationsRequired = _numConfirmationsRequired;
        transactionCount = 0;
    }

    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    function submitTransaction(address _to, uint _value, bytes memory _data) public onlyOwners {
        transactionCount++;
        transactions[transactionCount] = Transaction({
            to: _to,
            value: _value,
            data: _data,
            executed: false,
            numConfirmations: 0
        });

        emit Submission(transactionCount);
    }

    function confirmTransaction(uint _transactionId) public onlyOwners notExecuted(_transactionId) {
        require(!confirmations[_transactionId][msg.sender], "Transaction already confirmed by sender");

        confirmations[_transactionId][msg.sender] = true;
        transactions[_transactionId].numConfirmations++;

        emit Confirmation(msg.sender, _transactionId);
        executeTransaction(_transactionId); // Try to execute immediately after confirmation.
    }

    function revokeConfirmation(uint _transactionId) public onlyOwners notExecuted(_transactionId) {
        require(confirmations[_transactionId][msg.sender], "Transaction not confirmed by sender");

        confirmations[_transactionId][msg.sender] = false;
        transactions[_transactionId].numConfirmations--;

        emit Revocation(msg.sender, _transactionId);
    }

    function executeTransaction(uint _transactionId) public notExecuted(_transactionId) {
        Transaction storage transaction = transactions[_transactionId];
        require(transaction.numConfirmations >= numConfirmationsRequired, "Not enough confirmations");

        transaction.executed = true;

        (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
        if (success) {
            emit Execution(_transactionId);
        } else {
            emit ExecutionFailure(_transactionId);
        }
    }

    function getOwners() public view returns (address[] memory) {
        return owners;
    }

    function getTransactionCount() public view returns (uint) {
        return transactionCount;
    }

    function getTransaction(uint _transactionId) public view returns (address to, uint value, bytes memory data, bool executed, uint numConfirmations) {
        Transaction storage transaction = transactions[_transactionId];
        return (transaction.to, transaction.value, transaction.data, transaction.executed, transaction.numConfirmations);
    }

    function getConfirmationCount(uint _transactionId) public view returns (uint) {
        return transactions[_transactionId].numConfirmations;
    }

    function isConfirmed(uint _transactionId, address _owner) public view returns (bool) {
        return confirmations[_transactionId][_owner];
    }
}
```

**Explanation of Solidity Code:**

1.  **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.  Important to use a compatible version.

2.  **`contract MultiSigWallet { ... }`**: Defines the smart contract.

3.  **`address[] public owners;`**: An array of `address` representing the owners of the wallet.  `public` means you can access this variable directly from outside the contract.

4.  **`uint public numConfirmationsRequired;`**:  The number of confirmations required for a transaction to be executed.

5.  **`mapping(address => bool) public isOwner;`**: A mapping to efficiently check if an address is an owner.

6.  **`mapping(uint => Transaction) public transactions;`**:  A mapping to store transaction details, keyed by a transaction ID (an incrementing counter).

7.  **`uint public transactionCount;`**:  Keeps track of the total number of transactions submitted.  Serves as the transaction ID.

8.  **`mapping(uint => mapping(address => bool)) public confirmations;`**:  A nested mapping to track which owners have confirmed which transactions.

9.  **`struct Transaction { ... }`**:  Defines the structure for storing transaction details:
    *   `address to`:  The recipient of the transaction.
    *   `uint value`:  The amount of Ether to send.
    *   `bytes data`:  Arbitrary data to send along with the transaction (e.g., function call data).
    *   `bool executed`:  Indicates whether the transaction has been executed.
    *   `uint numConfirmations`: The number of confirmations received.

10. **`event Deposit(...)`**, **`event Submission(...)`**, **`event Confirmation(...)`**, **`event Revocation(...)`**, **`event Execution(...)`**, **`event ExecutionFailure(...)`**:  Events emitted to the blockchain for tracking purposes.  These are important for front-end applications to monitor the wallet's activity.

11. **`modifier onlyOwners() { ... }`**: A modifier to restrict function access to only the owners. `require(isOwner[msg.sender], "Sender is not an owner");` checks if the `msg.sender` (the address calling the function) is an owner.  If not, the function execution reverts. The `_;` indicates where the original function's code should be executed.

12. **`modifier notExecuted(uint transactionId) { ... }`**: A modifier to ensure a transaction hasn't already been executed.

13. **`modifier validRequirement(uint numOwners, uint _numConfirmationsRequired) { ... }`**: A modifier to ensure that the numConfirmationsRequired value is a valid number

14. **`constructor(address[] memory _owners, uint _numConfirmationsRequired)`**: The constructor.  It takes an array of owner addresses and the required number of confirmations as arguments.  It initializes the `owners` array and the `numConfirmationsRequired` variable. It validates that owner addresses are not zero, and are unique.
     *   It is also using the validRequirement modifier for validating the `numConfirmationsRequired`.

15. **`receive() external payable { ... }`**:  This function allows the wallet to receive Ether.  It's marked `payable` to allow Ether transfers.  It emits a `Deposit` event.

16. **`submitTransaction(address _to, uint _value, bytes memory _data) public onlyOwners { ... }`**:  Allows an owner to submit a new transaction.
    *   It increments `transactionCount`.
    *   It creates a new `Transaction` struct and stores it in the `transactions` mapping.
    *   It emits a `Submission` event.

17. **`confirmTransaction(uint _transactionId) public onlyOwners notExecuted(_transactionId) { ... }`**: Allows an owner to confirm a transaction.
    *   Checks that the sender has not already confirmed the transaction.
    *   Updates the `confirmations` mapping.
    *   Increments the `numConfirmations` counter for the transaction.
    *   Emits a `Confirmation` event.
    *   Calls `executeTransaction` to try to execute the transaction immediately after confirmation.

18. **`revokeConfirmation(uint _transactionId) public onlyOwners notExecuted(_transactionId) { ... }`**: Allows an owner to revoke a confirmation.

19. **`executeTransaction(uint _transactionId) public notExecuted(_transactionId) { ... }`**:  Executes a transaction if it has enough confirmations.
    *   Checks that the transaction has not already been executed.
    *   Checks that the number of confirmations is greater than or equal to `numConfirmationsRequired`.
    *   Sets `transaction.executed` to `true`.
    *   Uses `transaction.to.call{value: transaction.value}(transaction.data)` to make the external call. The `.call()` function is a low-level way to interact with other contracts or external addresses.  The `{value: transaction.value}` part sends Ether along with the call. `transaction.data` is the data payload for the call (e.g., function selector and arguments).
    *   Emits an `Execution` or `ExecutionFailure` event based on the success of the `call`.

20. **`getOwners() public view returns (address[] memory)`**:  Returns the list of owners.

21. **`getTransactionCount() public view returns (uint)`**:  Returns the total number of transactions.

22. **`getTransaction(uint _transactionId) public view returns (address to, uint value, bytes memory data, bool executed, uint numConfirmations)`**:  Returns the details of a specific transaction.

23. **`getConfirmationCount(uint _transactionId) public view returns (uint)`**: Returns the number of confirmations for a specific transaction.

24. **`isConfirmed(uint _transactionId, address _owner) public view returns (bool)`**:  Returns whether a specific owner has confirmed a specific transaction.

**JavaScript (Interaction Script): `interact.js`**

This JavaScript code will interact with the deployed smart contract.  You'll need Node.js and the `ethers` library.

```javascript
const { ethers } = require("ethers");
const MultiSigWalletJSON = require("./artifacts/contracts/MultiSigWallet.sol/MultiSigWallet.json"); //Adjust the path to your MultiSigWallet.json file

// Configuration (replace with your actual values)
const rpcUrl = "http://127.0.0.1:8545/"; // Your Ganache or Hardhat node URL
const privateKeys = [
  "0xac0974bec39a17e36ba4a6b4cdb30563e7c3cfc39ab61625269e2b6771489256", // Account 0
  "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7faaff90f92e56465ea5", // Account 1
  "0x5de469bc79bc91684eb892f6924ee26a4147491c2a2f1e61e5c794278869842f", // Account 2
]; // Private keys of the owners.  **NEVER** store private keys in a production application like this. Use a secure key management system.
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // Your deployed contract address
const requiredConfirmations = 2; // Minimum confirmations required
const transactionValueToSend = ethers.utils.parseEther("0.01"); // example 0.01 Ether to send.

async function main() {
  const provider = new ethers.providers.JsonRpcProvider(rpcUrl);

  // Example: Get accounts from private keys
  const owners = privateKeys.map((key) => new ethers.Wallet(key, provider).address);

  // Create a wallet for each owner (for signing transactions)
  const wallets = privateKeys.map((key) => new ethers.Wallet(key, provider));

  // Attach the contract to each wallet
  const multiSigWallets = wallets.map((wallet) => new ethers.Contract(contractAddress, MultiSigWalletJSON.abi, wallet.connect(provider)));

  // **********************************************************************
  // **********************************************************************
  // Deploying the contract if not already deployed, comment if not needed
  // **********************************************************************
  // **********************************************************************
  /*
  // Create a contract factory
  const factory = new ethers.ContractFactory(MultiSigWalletJSON.abi, MultiSigWalletJSON.bytecode, wallets[0].connect(provider));

  // Deploy the contract with the desired number of confirmations
  const contract = await factory.deploy(owners, requiredConfirmations);

  // Wait for the contract to be deployed
  await contract.deployed();

  // Log the contract address
  console.log("Contract deployed to:", contract.address);

  return; // end of deploy contract
  */
  // **********************************************************************
  // **********************************************************************

  // Example 1:  Submit a transaction (owner 1)
  const recipientAddress = "0xf39Fd6e51aad88F6F4ce6AB8827279cffFb92266"; // Example: Account 1
  const submitTx = await multiSigWallets[0].submitTransaction(recipientAddress, transactionValueToSend, "0x"); // 0x is empty data
  await submitTx.wait();
  console.log("Transaction submitted (owner 1)");

  // Get the current transaction count
  const transactionCount = await multiSigWallets[0].getTransactionCount();
  const transactionId = transactionCount.toNumber();
  console.log(`Current transaction ID: ${transactionId}`);

  // Example 2: Confirm a transaction (owner 2)
  const confirmTx = await multiSigWallets[1].confirmTransaction(transactionId);
  await confirmTx.wait();
  console.log("Transaction confirmed (owner 2)");

  // Example 3: Confirm a transaction (owner 3)
  const confirmTxOwner3 = await multiSigWallets[2].confirmTransaction(transactionId);
  await confirmTxOwner3.wait();
  console.log("Transaction confirmed (owner 3)");

  // Example 4: Get transaction details
  const [to, value, data, executed, numConfirmations] = await multiSigWallets[0].getTransaction(transactionId);
  console.log("Transaction Details:");
  console.log(`  To: ${to}`);
  console.log(`  Value: ${ethers.utils.formatEther(value)}`);
  console.log(`  Data: ${data}`);
  console.log(`  Executed: ${executed}`);
  console.log(`  Confirmations: ${numConfirmations}`);

  // Example 5: Check if a user has confirmed a transaction
  const hasConfirmed = await multiSigWallets[0].isConfirmed(transactionId, wallets[0].address);
  console.log(`Owner 1 has confirmed transaction ${transactionId}: ${hasConfirmed}`);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

**Explanation of JavaScript Code:**

1.  **`const { ethers } = require("ethers");`**: Imports the `ethers` library.  Make sure you have installed it: `npm install ethers`.

2.  **`const MultiSigWalletJSON = require("./artifacts/contracts/MultiSigWallet.sol/MultiSigWallet.json");`**:  Imports the ABI (Application Binary Interface) of the smart contract.  The ABI is a JSON file that describes the contract's functions, events, and data structures, allowing JavaScript to interact with the contract.  **Adjust the path to your `MultiSigWallet.json` file.**  This file is generated by the Solidity compiler when you compile your smart contract.

3.  **Configuration**:  Sets up the necessary configuration parameters:
    *   `rpcUrl`:  The URL of your Ethereum node (e.g., Ganache or Hardhat node).
    *   `privateKeys`: An array containing the private keys of the owners of the multisig wallet. **Important:** Never store private keys directly in your code in a production environment. Use a secure key management system.
    *   `contractAddress`: The address of the deployed smart contract.
    *   `requiredConfirmations`: The number of confirmations required for a transaction to execute.
    *   `transactionValueToSend`: Amount of Ether to send on a transaction.

4.  **`async function main() { ... }`**:  The main function, which contains the core interaction logic.

5.  **`const provider = new ethers.providers.JsonRpcProvider(rpcUrl);`**: Creates an `ethers.providers.JsonRpcProvider` instance to connect to the Ethereum node.

6.  **`const owners = privateKeys.map((key) => new ethers.Wallet(key, provider).address);`**:  Maps over the array of private keys to get the public address of each owner.

7.  **`const wallets = privateKeys.map((key) => new ethers.Wallet(key, provider));`**:  Creates `ethers.Wallet` objects for each owner using their private keys. Wallets are used for signing transactions.

8.  **`const multiSigWallets = wallets.map((wallet) => new ethers.Contract(contractAddress, MultiSigWalletJSON.abi, wallet.connect(provider)));`**:  Creates `ethers.Contract` instances for each owner, connecting them to the deployed smart contract using its address and ABI.  The `wallet.connect(provider)` part ensures that each contract instance is associated with the correct owner's wallet.

9.  **Deploying the contract (Optional)**: If the contract is not yet deployed, you can uncomment the block to deploy the contract.
    *   **`const factory = new ethers.ContractFactory(MultiSigWalletJSON.abi, MultiSigWalletJSON.bytecode, wallets[0].connect(provider));`**: Create a contract factory to deploy the contract
    *   **`const contract = await factory.deploy(owners, requiredConfirmations);`**: Deploy the contract using the owners array and the numConfirmationsRequired value
    *   **`await contract.deployed();`**: Wait for the contract to be deployed
    *   **`console.log("Contract deployed to:", contract.address);`**: Print the contract address
    *   **`return; // end of deploy contract`**: Finish this execution.

10. **Example 1: Submit a transaction (owner 1)**: Submits a new transaction to the smart contract.  This example sends 0.01 Ether to the address in `recipientAddress`. The data is empty (`"0x"`).
    *   **`const submitTx = await multiSigWallets[0].submitTransaction(recipientAddress, transactionValueToSend, "0x");`**: Calls the `submitTransaction` function of the contract using the first wallet.
    *   **`await submitTx.wait();`**:  Waits for the transaction to be mined.
    *   **`console.log("Transaction submitted (owner 1)");`**: Logs a message to the console.

11. **Getting the transaction count**
    *   **`const transactionCount = await multiSigWallets[0].getTransactionCount();`**: Calls the `getTransactionCount` to get the current number of transactions.
    *   **`const transactionId = transactionCount.toNumber();`**: Convert the transaction count from a `BigNumber` to a regular number.
    *   **`console.log(`Current transaction ID: ${transactionId}`);`**: Prints the transaction Id.

12. **Example 2 & 3: Confirm a transaction (owner 2 and 3)**:  Confirms the transaction using the second and third owner's wallets.
    *   **`const confirmTx = await multiSigWallets[1].confirmTransaction(transactionId);`**: Calls the `confirmTransaction` function of the contract using the second wallet.
    *   **`await confirmTx.wait();`**: Waits for the transaction to be mined.
    *   **`console.log("Transaction confirmed (owner 2)");`**: Logs a message to the console.

13. **Example 4: Get transaction details**: Retrieves the details of the submitted transaction using the `getTransaction` function.

14. **Example 5: Check if a user has confirmed a transaction**: Check if owner 1 has confirmed transaction with the `isConfirmed` function.

**How to Run:**

1.  **Set up your environment:**
    *   Install Node.js:  [https://nodejs.org/](https://nodejs.org/)
    *   Install Ganache or Hardhat:
        *   Ganache: `npm install ganache -g` (for a local, GUI-based blockchain)
        *   Hardhat: `npm install --save-dev hardhat` (for a more configurable development environment)
    *   Install `ethers`: `npm install ethers`

2.  **Compile the Solidity contract:**
    *   If using Hardhat: `npx hardhat compile`
    *   If using Remix: Remix will compile automatically.

3.  **Deploy the contract:**
    *   **Ganache:** Deploy using Remix or Truffle. In Remix, connect to the Ganache provider using MetaMask (or a similar wallet). Copy the deployed contract address.
    *   **Hardhat:**
        *   Create a Hardhat task or script to deploy the contract.  See the Hardhat documentation for details.
        *   Run the deployment script (e.g., `npx hardhat run scripts/deploy.js --network localhost`).  Copy the deployed contract address.

4.  **Update the configuration in `interact.js`:**
    *   Replace `rpcUrl` with the URL of your Ganache or Hardhat node.
    *   Replace the `privateKeys` with the private keys of the addresses you want to use as owners.  **Again, NEVER store private keys in production code.**  Use a secure key management system.
    *   Replace `contractAddress` with the address of your deployed contract.
    *   Update `requiredConfirmations` to the desired number.
    *   Modify the `recipientAddress` for the transaction.

5.  **Run the JavaScript script:**
    *   `node interact.js`

**Important Considerations and Security:**

*   **Private Key Management:**  The biggest security risk is storing private keys directly in your code.  For production, use a hardware wallet (like Ledger or Trezor), a secure key management service (like AWS KMS or HashiCorp Vault), or a more sophisticated signing solution.
*   **Re-entrancy:**  This wallet is not protected against re-entrancy attacks.  A malicious contract could potentially call back into the wallet during the `executeTransaction` function, potentially draining funds.  Consider using re-entrancy protection patterns (e.g., using a mutex).
*   **Gas Optimization:** The code isn't heavily optimized for gas usage. Consider optimizing loops, data storage, and function calls to reduce gas costs.
*   **Error Handling:** The `executeTransaction` function only emits an `ExecutionFailure` event. You might want to provide more detailed error information to help with debugging.
*   **Events:** Make sure to emit events for all important state changes, so that external applications can easily monitor the wallet's activity.
*   **Testing:** Thoroughly test your smart contract with different scenarios and edge cases before deploying it to a production environment.

This example provides a basic foundation. Building a secure and robust multi-signature wallet requires careful attention to detail, security audits, and best practices. Remember to always prioritize security when working with smart contracts and cryptocurrency.
👁️ Viewed: 9

Comments