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.