Hardhat is a powerful development environment for Ethereum that facilitates the creation, testing, and deployment of smart contracts. In a multi-signature wallet project, Hardhat plays a crucial role in managing the development lifecycle, including writing, testing, and deploying the multi-signature wallet smart contract. This guide will explain the role of Hardhat in such a project and provide sample code to demonstrate its usage.

What is a Multi-Signature Wallet?

A multi-signature wallet (or multi-sig wallet) requires multiple signatures (or approvals) to execute transactions. This enhances security by ensuring that no single individual has control over the wallet's funds. Multi-sig wallets are commonly used by organizations and DAOs (Decentralized Autonomous Organizations) to manage funds collectively.

Setting Up Your Hardhat Project

To start, you need to set up a Hardhat project. If you haven't done this yet, follow these steps:

mkdir multi-sig-wallet
cd multi-sig-wallet
npm init --yes
npm install --save-dev hardhat
npx hardhat

When prompted, select "Create a basic sample project" and follow the instructions to set up the project.

Creating the Multi-Signature Wallet Contract

Create a new file in the contracts directory named MultiSigWallet.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MultiSigWallet {
event Deposit(address indexed sender, uint amount);
event Submit(uint indexed txIndex, address indexed owner, address indexed to, uint amount, bytes data);
event Confirm(uint indexed txIndex, address indexed owner);
event Revoke(uint indexed txIndex, address indexed owner);
event Execute(uint indexed txIndex);

address[] public owners;
mapping(address => bool) public isOwner;

struct Transaction {
address to;
uint amount;
bytes data;
bool executed;
uint confirmations;
}

Transaction[] public transactions;
mapping(uint => mapping(address => bool)) public isConfirmed;

modifier onlyOwner() {
require(isOwner[msg.sender], "Not an owner");
_;
}

constructor(address[] memory _owners) {
require(_owners.length > 0, "No owners");
for (uint i = 0; i < _owners.length; i++) {
require(!isOwner[_owners[i]], "Owner is not unique");
isOwner[_owners[i]] = true;
owners.push(_owners[i]);
}
}

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

function submitTransaction(address _to, uint _amount, bytes memory _data) public onlyOwner {
uint txIndex = transactions.length;
transactions.push(Transaction({
to: _to,
amount: _amount,
data: _data,
executed: false,
confirmations: 0
}));
emit Submit(txIndex, msg.sender, _to, _amount, _data);
}

function confirmTransaction(uint _txIndex) public onlyOwner {
require(!isConfirmed[_txIndex][msg.sender], "Transaction already confirmed");
isConfirmed[_txIndex][msg.sender] = true;
transactions[_txIndex].confirmations += 1;
emit Confirm(_txIndex, msg.sender);
}

function executeTransaction(uint _txIndex) public onlyOwner {
Transaction storage transaction = transactions[_txIndex];
require(transaction.confirmations > 1, "Not enough confirmations");
require(!transaction.executed, "Transaction already executed");

transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.amount}(transaction.data);
require(success, "Transaction failed");
emit Execute(_txIndex);
}
}

Writing Tests for the Multi-Signature Wallet

Hardhat provides a testing framework to ensure that your smart contracts behave as expected. Create a new test file in the test directory named MultiSigWallet.test.js:

const { expect } = require("chai");

describe("MultiSigWallet", function () {
let MultiSigWallet;
let multiSigWallet;
let owner1, owner2, owner3, addr1;

beforeEach(async function () {
[owner1, owner2, owner3, addr1] = await ethers.getSigners();
const owners = [owner1.address, owner2.address, owner3.address];
MultiSigWallet = await ethers.getContractFactory("MultiSigWallet");
multiSigWallet = await MultiSigWallet.deploy(owners);
await multiSigWallet.deployed();
});

it("should allow owners to submit transactions", async function () {
await multiSigWallet.submitTransaction(addr1.address, ethers.utils.parseEther("1"), "0x");
const transaction = await multiSigWallet.transactions(0);
expect(transaction.to).to.equal(addr1.address);
expect(transaction.amount).to.equal(ethers.utils.parseEther("1"));
});

it("should allow owners to confirm transactions", async function () {
await multiSigWallet.submitTransaction(addr1.address, ethers.utils.parseEther("1"), "0x");
await multiSigWallet.connect(owner1).confirmTransaction(0);
const transaction = await multiSigWallet.transactions(0);
expect(transaction.confirmations).to.equal(1);
});

it("should execute transaction with enough confirmations", async function () {
await multiSigWallet.submitTransaction(addr1.address, ethers.utils.parseEther("1"), "0x");
await multiSigWallet.connect(owner1).confirmTransaction(0);
await multiSigWallet.connect(owner2).confirmTransaction(0);
await multiSigWallet.executeTransaction(0);
const balance = await ethers.provider.getBalance(addr1.address);
expect(balance).to.equal(ethers.utils.parseEther("1"));
});
});

Deploying the Multi-Signature Wallet

To deploy the multi-signature wallet, create a new deployment script in the scripts directory named deploy.js:

async function main() {
const [owner1, owner2, owner3] = await ethers.getSigners();
const owners = [owner1.address, owner2.address, owner3.address];
const MultiSigWallet = await ethers.getContractFactory("MultiSigWallet");
const multiSigWallet = await MultiSigWallet.deploy(owners);
await multiSigWallet.deployed();
console.log("MultiSigWallet deployed to:", multiSigWallet.address);
}

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

Conclusion

In this guide, we explored the role of Hardhat in developing a multi-signature wallet project. We set up a Hardhat project, created a multi-signature wallet contract, wrote tests to ensure its functionality, and deployed the contract. Hardhat's powerful features streamline the development process, making it easier to build secure and reliable smart contracts.