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