I pasted my first attempt below.
Mappings brought me a few headaches because they cannot be iterated and they don’t have a length, therefore I eventually reverted to dynamic arrays in places. I guess it is fine since the size of the arrays is small.
The different owner operations are separated (request a transfer, approve a transfer, execute a transfer). I was tempted to cascade some of them but eventually gave up because it increases complexity. For instance
- requesting a transfer does not auto-approve;
- approving a transfer does not auto-execute if all approvals are present.
I’m curious to look at the solution now!
I’m also curious to find out how to write unit-tests in Solidity.
Matt
pragma solidity 0.7.5;
pragma abicoder v2;
contract MultisigWallet{
mapping( address => bool) ownersMapping;
address[] owners;
uint minApprovals;
struct Request{
address payable recipient;
uint amount;
address[] approvals;
bool transferDone;
}
Request[] requests;
modifier onlyOwner{
require(ownersMapping[msg.sender] == true, "Can be called only by one of the owners");
_;
}
modifier validRequestId(uint requestId){
require(requestId < requests.length, "Invalid request id");
_;
}
constructor(address[] memory _owners, uint _minApprovals){
require(_owners.length <= 5, "No more than 5 owners");
require(_owners.length >= 1, "Specify at least 1 owner");
require(_minApprovals <= _owners.length, "There cannot be more approvers than owners");
require(_minApprovals >= 1, "There should be at least 1 approver");
for(uint i=0; i < _owners.length; ++i){
ownersMapping[_owners[i]] = true;
}
owners = _owners;
minApprovals = _minApprovals;
}
// Add funds to the wallet
function deposit() external payable returns (uint balance){
return address(this).balance;
}
//Log a transfer request and return a request id to be used for approval and execution of the transfer
function requestTransfer(address payable recipient, uint amount) external onlyOwner returns (uint requestId){
require(address(this).balance >= amount, "Not enough balance!");
Request storage newRequest = requests.push();
newRequest.recipient = recipient;
newRequest.amount = amount;
newRequest.transferDone = false;
// Return request id to be used to approve
return requests.length-1;
}
// Approve transfer request by request id. Returns number of missing approvals
function approveTransfer(uint requestId) external onlyOwner validRequestId(requestId) returns (uint missingApprovals) {
Request storage requestToApprove = requests[requestId];
require(requestToApprove.transferDone == false, "This transfer request was already executed");
require(!hasApproved(requestId, msg.sender), "You already approved this request");
requestToApprove.approvals.push(msg.sender);
assert(requestToApprove.approvals.length <= owners.length);
return (minApprovals-requestToApprove.approvals.length);
}
// Perform the actual transfer provided all approvals have been received
function executeTransfer(uint requestId) public onlyOwner validRequestId(requestId) returns (uint newBalance){
Request storage requestToExecute = requests[requestId];
require(requestToExecute.transferDone == false, "This transfer request was already executed");
//If min number of approvals is reached, execute the transfer
uint _approvals = getApprovals(requestId);
require(_approvals >= minApprovals, "Missing approvals");
requestToExecute.transferDone = true;
uint oldBalance = address(this).balance;
_transfer(requestToExecute);
assert( (oldBalance - address(this).balance ) == requestToExecute.amount);
return address(this).balance;
}
// Return the number of transfer requests that have not yet been executed
function pendingRequests() public view returns (uint){
uint _pending = 0;
for(uint i=0; i< requests.length; ++i){
if (requests[i].transferDone == false){
_pending++;
}
}
return _pending;
}
// Return the number of approvals given by owners to the specified request
function getApprovals(uint requestId) public view validRequestId(requestId) returns (uint approvals){
Request storage requestToApprove = requests[requestId];
return requestToApprove.approvals.length;
}
// Return true if owner has approved the given request id
function hasApproved(uint requestId, address owner) public view validRequestId(requestId) returns (bool approved){
require(requestId < requests.length, "Invalid request id");
Request storage requestToApprove = requests[requestId];
for(uint i=0; i< requestToApprove.approvals.length; ++i){
if (requestToApprove.approvals[i] == owner){
return true;
}
}
return false;
}
// Return the balance of the wallet
function walletBalance() public view returns (uint balance){
return address(this).balance;
}
function _transfer(Request storage request) private {
require(request.amount <= address(this).balance, "Not enough balance to execute the transfer ");
request.transferDone = true;
request.recipient.transfer(request.amount);
}
}