Multi-Signature Wallet (Solidity)

FusionTech | Academy
5 min readSep 22, 2023

--

https://fusiontech.live/

Imagine a organization reaches out to you as a smart contract developer, they want a contract where ethers (payment) can be received based on services they rendered.

Now the organization has a board of directors, and before withdrawal can be made from the contract, 70% of the total number of directors will need to approve such a transaction.

In this article, I will be showing you how to create a smart contract that handles such demands.

Let’s get into action. Set up a folder and add a MultiSig.sol file.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

contract MultiSig {
}

As a seasoned smart contract developer, you know what the snippet is. For beginners, the first line is an identifier to denote the License of our smart contract. The second line starts with the pragma keyword and the solidity compiler version. The last line is just a definition of our contract.

Let’s define the data types, errors handling and modifier for our contract. I will place comments to aid in explanation.

//transaction struct holds the details of any of our admin that 
//makes a request for withdrawal
struct Transaction {
address spender;
uint amount;
uint numberOfApproval;
bool isActive;
}

//an array of admins (board of directors)
address[] Admins;

//minimum number for admins
uint constant MINIMUM = 3;

//transaction identifier
uint transactionId;

//a mapping to check if an address is that of an identifier
mapping(address => bool) isAdmin;

//a mapping of a transaction identifier to a Transaction
mapping(uint => Transaction) transaction;

//a 2D mapping to check if an address(admin) has already
//approved a transaction, to avoid a waste of gas
mapping(uint => mapping(address => bool)) hasApproved;

//a customized error check for invalid address, when adding admins
error InvalidAddress(uint position);

//a customized error check for minimum admins to be added
error InvalidAdminNumber(uint number);

//a customized error check for minimum admins to be added
error duplicate(address _addr);

//an event to be emitted, when a withdrawal request is made
event Create(address who, address spender, uint amount);

//a check to ensure that only an admin can proceed on an action
modifier onlyAdmin() {
require(isAdmin[msg.sender], "Not a Valid Admin");
_;
}

Good job so far, we have created the layout of our contract.

Now, we want our contract to contain admins before deployment and to achieve that, we will need to add the logic to our constructor. The code defined inside the constructor will run only once, at the time the contract is created and deployed in the network.

constructor(address[] memory _admins) payable {
//checks to make sure that admins to be added
//are more than 2
if (_admins.length < MINIMUM) {
revert InvalidAdminNumber(MINIMUM);
}

for (uint i = 0; i < _admins.length; i++) {
//checks to make sure that none of the addresses
//is a zero address
if (_admins[i] == address(0)) {
revert InvalidAddress(i + 1);
}

//checks to make sure no duplicates in addresses
if (isAdmin[_admins[i]]) {
revert duplicate(_admins[i]);
}

//maps provided address to true value
isAdmin[_admins[i]] = true;
}

//set Admins to the addresses of admins provided
Admins = _admins;
}

What are we doing with the code snippet? We are basically telling the contract to add the admins (addresses) we will provide before deployment. We have a check that reverts the process if provided address is below our minimum number state variable declared.

We also have a check for duplicated addresses and zero address. Inside our constructor, we set the admins before deployment.

We will be having five(5) functions as bases in our smart contract (You can decide to add more based on the requirements you intend to implement).

Lets define them;

function createTransaction(uint amount, address _spender) external onlyOwner {}
function aprroveTransaction(uint id) external {}
function sendtransaction(uint id) internal {}
function calculateMinimumApproval() internal {}
function getTransaction(uint id) external view returns (Transaction memory){}

So, we will start with the createTransaction function. Basically, the function will have amount and spender as input, only admins can create a transaction (withdrawal).

function createTransaction(
uint amount,
address _spender
) external onlyAdmin {

//Require amount to be withdrawn is less than the contract balance
require(amount <= address(this).balance, "Insufficient Funds!!");
transactionId++;
Transaction storage _transaction = transaction[transactionId];
_transaction.amount = amount;
_transaction.spender = _spender;
_transaction.isActive = true;

//emit create event
emit Create(msg.sender, _spender, amount);

//the admin that creates a transaction, is the first to approve such
//transaction
AprroveTransaction(transactionId);
}

Using the code snippet above, the contract takes in two args (spender and amount). We have our modifier for only admin, we also check if the amount is not more than the balance of our contract. We then create our transaction using the Transaction struct we defined earlier. Emit our create event.

The next function is the approve transaction, where admins can approve transaction.

function AprroveTransaction(uint id) public onlyAdmin {
//check if addmin has not approved yet
require(!hasApproved[id][msg.sender], "Already Approved!!");

Transaction storage _transaction = transaction[id];

//require transaction to be active (exist)
require(_transaction.isActive, "Not active");

//increase transaction approvals
_transaction.numberOfApproval += 1;
hasApproved[id][msg.sender] = true;
uint count = _transaction.numberOfApproval;

//a mini method, that calculates if the number of approvals
//is up to 70%, then transaction is approved finally and ethers sent
uint MinApp = calculateMinimumApproval();
if (count >= MinApp) {
sendtransaction(id);
}
}

Let’s add functions that send transaction (once 70% of admins have approved) and the one that calculates the seventy percent. Both functions will be private (only accessible within the contract it is defined).

function sendtransaction(uint id) private {
Transaction storage _transaction = transaction[id];
payable(_transaction.spender).transfer(_transaction.amount);
_transaction.isActive = false;
}

function calculateMinimumApproval() private view returns (uint MinApp) {
uint size = Admins.length;
MinApp = (size * 70) / 100;
}

Then our last but not the least function, a function to help read the current status of an existing transaction. This will help any of the admins to view the status of a transaction. Details such as spender, number of current approvals, amount requested, and status of transaction can be viewed.

function getTransaction(
uint id
) external view returns (Transaction memory) {
return transaction[id];
}

Remember, our contract is a contract that receives ethers, and so we should add a receive function.

The receive function is similar to the fallback function, but it is designed specifically to handle incoming ether without the need for a data call. It is not a required function for a contract to have, but it can be useful for handling ether that is sent directly to the contract without a function call.

receive() external payable{}

Great job. By following the steps above, You have a created a smart contract that recieves ethers, and then only allow withdrawal, only when a condition is met (approval of seventy percent of the admins). You can build on this contract, by extending its functionalities and logic.

Drop a comment, clap, and follow for more articles that give detailed description of smart contracts, web3 concepts, solidity, etc.

--

--

FusionTech | Academy
FusionTech | Academy

Written by FusionTech | Academy

0 Followers

We are a trained professional providing independent and objective evaluations of blockchain and every audit process is unique.

No responses yet