Here I created a Multisig Wallet, as per spec. I added events for loggings, asserts to validate that the code is working as expected, and requires to validate some edge cases I found.
In this case, the requester should send the request first, then sign it. I did that to avoid the case when the request is sent, but the contract has not enough funds to continue.
pragma solidity 0.7.5;
pragma abicoder v2;
contract MultisigWallet
{
//I will copy the CrowdFunding approach in the Structs section in https://docs.soliditylang.org/en/v0.7.5/types.html
//This RequestStatus ENUM will track TransactionRequest Status
enum RequestStatus { Pending, Sent }
//Here we define the TransactionRequest struct, that will have a signatures mapping to avoid users to sign more than once
struct TransactionRequest
{
address requester;
address payable receiver;
uint amount;
uint approvalSignatures;
RequestStatus status;
mapping (address => bool) signatures;
}
//Here we create a mapping that will simulate the behavior of an array
uint public numTransactionRequests;
mapping (uint => TransactionRequest) transactionRequestLog;
//Here we define owners mapping (to check if the sender is owner in a O(1) answer lookup, and the required amount of signatures to execute the transfer
mapping(address => bool) private owners;
uint private requiredSignatures;
//This variable will store the balance, to reduce gas usage (asking for address balance)
uint private contractBalance;
//Here we define events as contract logs
event depositAdded(uint amount, address indexed sender);
event transactionRequestAdded(uint index, uint amount, address indexed receiver, address indexed sender);
event transactionRequestSignature(uint index, address indexed signedBy);
event transactionRequestExecuted(uint index, uint amount, address indexed receiver);
//Here we define a modifier to check ownership, in order to restrict access to certain methods
modifier onlyOwner
{
require(owners[msg.sender], "You are not allowed to perform this operation");
_; // run the function
}
/*
The contract creator should be able to input
(1): the addresses of the owners and
(2): the numbers of approvals required for a transfer, in the constructor.
For example, input 3 addresses and set the approval limit to 2.
In my remix account, I deployed using the following parameters:
_owners: ["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]
_requiredSignatures: 2
*/
constructor(address[] memory _owners, uint _requiredSignatures)
{
require(_owners.length > 0, "The wallet needs at least one owner's address");
require(_requiredSignatures > 0, "The amount of required signatures cannot be smaller than 1");
require(_requiredSignatures < _owners.length + 1, "The amount of required signatures cannot be bigger than the amount of owners");
requiredSignatures = _requiredSignatures;
//We should verify that all addresses are different.
bool areAllUniqueAddresses = true;
for(uint index = 0; index < _owners.length; index++)
{
if(owners[_owners[index]])
{
areAllUniqueAddresses = false;
break;
}
owners[_owners[index]] = true;
}
require(areAllUniqueAddresses, "Owners' addresses should be unique");
}
//Anyone should be able to deposit ether into the smart contract
function deposit() public payable returns (uint)
{
uint previousContractBalance = contractBalance;
contractBalance += msg.value;
emit depositAdded(msg.value, msg.sender);
assert(contractBalance == previousContractBalance + msg.value);
return getBalance();
}
//We will use a state variable for the contractBalance to reduce gas expenses
function getBalance() public view returns (uint)
{
//return address(this).balance;
return contractBalance;
}
/*
Anyone of the owners should be able to create a transfer request. (It will be done using the onlyOwner modifier)
The creator of the transfer request will specify what amount and to what address the transfer will be made
It will return the request index, to avoid using other methods such as getTransferRequest or getTransferRequestList
*/
function transferRequest(address payable _receiver, uint _amount) public onlyOwner returns (uint)
{
require(getBalance() >= _amount, "Error: Not enough funds");
uint transactionRequestId = numTransactionRequests;
//In the current approach we can't initialize the struct with a constructor, since it has a mapping
TransactionRequest storage request = transactionRequestLog[transactionRequestId];
request.requester = msg.sender;
request.receiver = _receiver;
request.amount = _amount;
request.approvalSignatures = 0;
request.status = RequestStatus.Pending;
numTransactionRequests++;
assert(numTransactionRequests == transactionRequestId + 1);
assert(transactionRequestLog[transactionRequestId].requester != address(0x0));
emit transactionRequestAdded(transactionRequestId, _amount, _receiver, msg.sender);
return transactionRequestId;
}
/*
- Owners should be able to approve transfer requests.
- When a transfer request has the required approvals, the transfer should be sent.
We will return the request status, to give a feedback of the request progression
*/
function approveTransferRequest(uint _index) public onlyOwner returns (RequestStatus)
{
require(_index < numTransactionRequests, "Error: The transaction request does not exist");
require(transactionRequestLog[_index].status == RequestStatus.Pending, "Error: The transaction request have been already approved and funds were delivered");
require(transactionRequestLog[_index].signatures[msg.sender] == false, "Error: You have already signed this request");
require(getBalance() >= transactionRequestLog[_index].amount, "Error: The account has not enough funds to approve this transaction");
uint previousSignatures = transactionRequestLog[_index].approvalSignatures;
transactionRequestLog[_index].approvalSignatures++;
transactionRequestLog[_index].signatures[msg.sender] = true;
assert(transactionRequestLog[_index].approvalSignatures == previousSignatures + 1);
assert(transactionRequestLog[_index].signatures[msg.sender]);
emit transactionRequestSignature(_index, msg.sender);
//If we have the required signatures, we will generate the transfer
if(transactionRequestLog[_index].approvalSignatures == requiredSignatures)
{
executeTransferRequest(_index);
}
return transactionRequestLog[_index].status;
}
// Validations were performed in previous methods to avoid transfer failures due to insufficient funds
function executeTransferRequest(uint _index) private
{
uint previousContractBalance = contractBalance;
contractBalance -= transactionRequestLog[_index].amount;
transactionRequestLog[_index].status = RequestStatus.Sent;
//I reordered the code, putting the transfer function after the balance and status modifications, for security reasons (applying the fixes from the previous assignment)
(transactionRequestLog[_index].receiver).transfer(transactionRequestLog[_index].amount);
assert(contractBalance == previousContractBalance - transactionRequestLog[_index].amount);
assert(transactionRequestLog[_index].status == RequestStatus.Sent);
emit transactionRequestExecuted(_index, transactionRequestLog[_index].amount, transactionRequestLog[_index].receiver);
}
//this method allow owners to check what is the content of the transaction
function getTransferRequest(uint _index) public view onlyOwner returns (address, address, uint, uint)
{
require(_index < numTransactionRequests, "Error: The transaction request does not exist");
return (transactionRequestLog[_index].requester, transactionRequestLog[_index].receiver, transactionRequestLog[_index].amount, transactionRequestLog[_index].approvalSignatures);
}
}