My first attempt, before watching any of the hints.
This multisig wallet has 3 authorised signatories, and the creator can choose to require 2/3 or 3/3 approvals for each transfer request to be sent.
Where only 2/3 approvals are required, if the beneficiary is one of the authorised signatories, the other 2 signatories must approve the transfer request for it to be sent.
Where 3/3 approvals are required, if the beneficiary is one of the authorised signatories, only the other 2 signatories must approve the transfer request for it to be sent.
An authorised signatory can create a new transfer request with themselves as the beneficiary and, in doing so, will be recorded as the 1st authorised signatory in the approval process for that transfer. However, as the other 2 signatories must then go on to approve such a transfer request for it to be sent, the 1st authorised signatory will not be counted as one of the 2 approvals required in these cases.
ownable.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
contract Ownable {
address public owner; // contract creator
modifier onlyOwner {
require(msg.sender == owner, "Operation restricted to contract creator");
_;
}
constructor() {
owner = msg.sender;
}
}
approval.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
import "./ownable.sol";
contract Approval is Ownable {
// 3 signatories authorised to approve transfers, set by constructor
address[3] public auth;
// 2 or all 3 of the authorised signatories required to approve each transfer,
// set by constructor
uint8 public approvals;
event Approvals(uint8 required);
event SignatoryChanged(address removedAuth, address newAuth);
modifier onlyAuth {
require(
msg.sender == auth[0] || msg.sender == auth[1] || msg.sender == auth[2],
"Operation restricted to authorised signatories"
);
_;
}
constructor(uint8 _approvals, address _auth1, address _auth2, address _auth3) {
require(
_approvals == 2 || _approvals == 3,
"Required approvals must be 2 or 3 (out of 3)"
);
require(
_auth1 != _auth2 && _auth1 != _auth3 && _auth2 != _auth3,
"Each authorised signatory must be a different address"
);
approvals = _approvals;
auth[0] = _auth1;
auth[1] = _auth2;
auth[2] = _auth3;
}
function changeRequiredApprovals() external onlyOwner {
approvals == 3 ? approvals = 2 : approvals = 3;
emit Approvals(approvals);
}
function changeSignatory(uint8 authNum, address newAuth) external onlyOwner {
require(authNum >= 1 && authNum <= 3, "Select signatory 1, 2 or 3");
require(
newAuth != auth[authNum - 1],
"New authorised signatory should be different from the one it is replacing"
);
require(
newAuth != auth[0] && newAuth != auth[1] && newAuth != auth[2],
"New authorised signatory must be different from the other two"
);
address priorAuth = auth[authNum - 1];
auth[authNum - 1] = newAuth;
emit SignatoryChanged(priorAuth, auth[authNum - 1]);
}
}
multisigWallet.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
// pragma abicoder v2;
import "./approval.sol";
contract MultisigWallet is Approval {
constructor(uint8 _approvals, address _auth1, address _auth2, address _auth3)
Approval(_approvals, _auth1, _auth2, _auth3) {}
struct Request {
bool pending;
address beneficiary;
uint256 amount;
uint8 stillRequired; // Number of approvals still required
address sig1;
address sig2;
address rejected; /* Records signatory if they reject a transfer request but it
remains "Pending". This can only occur where 2 out of 3 approvals are required */
}
// Stores a transfer request pending further approval, until either sent or fully rejected
Request private request;
event Transfer(
string status, uint8 stillRequired, address auth, address beneficiary, uint256 amount
);
function getBalance() external view returns(uint256 balance) {
return address(this).balance;
}
function deposit() external payable {}
/* Any of the 3 authorised signatories can create a new transfer request and provide
1st approval, pending 1 or 2 further approvals from the other signatories.
(Note, no 1st approval is counted at this stage when the creator of the transfer
request is also the beneficiary, as explained in the introductory notes) */
function createRequest(address _beneficiary, uint256 _amount) external onlyAuth {
// Can only create a new request if there isn't one already pending
require(!request.pending, "There is already a request awaiting approval");
/* The transfer method would throw an error and revert the approveOrReject()
function if it eventually attempted to send a transfer when there was an
insufficient contract balance to cover it.
But this contract balance check is performed when transfer requests are
created, in order to prevent those which are likely to fail from going
through the approval process unnecessarily and wasting time, gas and storage */
require(address(this).balance >= _amount, "Insufficient funds");
request.pending = true;
request.beneficiary = _beneficiary;
request.amount = _amount;
request.sig1 = msg.sender;
// Establishes number of approvals still required
if (
/* If 2/3 approvals required, and creator of new request is also
the beneficiary, then 2 further approvals still required */
approvals == 2 && _beneficiary != msg.sender ||
/* If all 3 approvals required, but one of other 2 signatories is also the
beneficiary, then only 1 further approval required (i.e. total reduced to 2 */
approvals == 3 && _beneficiary != msg.sender &&
(_beneficiary == auth[0] || _beneficiary == auth[1] || _beneficiary == auth[2])
) {
request.stillRequired = 1;
}
else request.stillRequired = 2;
emit Transfer("Pending", request.stillRequired, msg.sender, _beneficiary, _amount);
}
/* An authorised signatory can check if there is a transfer request
awaiting their approval, and can view the details if there is */
function needsMyApproval() external view onlyAuth returns(
address beneficiary, uint256 amount, uint256 stillRequired,
address sig1, address sig2, address notSignedBy
) {
require(request.pending, "There is no pending transfer request");
require(
request.beneficiary != msg.sender &&
request.sig1 != msg.sender &&
request.sig2 != msg.sender &&
request.rejected != msg.sender,
"There is no transfer request awaiting your approval"
);
return (
request.beneficiary, request.amount, request.stillRequired,
request.sig1, request.sig2, request.rejected
);
}
/* Any address can get details of a pending transfer request,
whether or not it requires their approval */
function getPendingRequest() external view returns(
address beneficiary, uint256 amount, uint256 stillRequired,
address sig1, address sig2, address notSignedBy
) {
require(request.pending, "There is no pending transfer request");
return (
request.beneficiary, request.amount, request.stillRequired,
request.sig1, request.sig2, request.rejected
);
}
/*
// Same function, but returning whole struct instance instead of separate values
// Need to uncomment pragma abicoder v2;
function getPendingRequest() external view returns(Request memory) {
require(request.pending, "There is no pending transfer request");
return request;
}
*/
// Other authorised signatories approve or reject a pending transfer request
// Boolean parameter: true (approve/sign), false (reject/don't sign)
function approveOrReject(bool approve) external onlyAuth {
require(request.pending, "There is no pending transfer request");
require(request.rejected != msg.sender, "You have already rejected this transfer request");
require(
request.beneficiary != msg.sender,
"You cannot approve or reject this transfer request, because you are the beneficiary"
);
require(
request.sig1 != msg.sender && request.sig2 != msg.sender,
"You have already approved this transfer request"
);
address recipient = request.beneficiary;
if (
approvals == 3 || approve || request.rejected != address(0) ||
recipient == auth[0] || recipient == auth[1] || recipient == auth[2]
) {
request.stillRequired--;
}
uint256 amount = request.amount;
string memory status;
if (request.stillRequired == 0 && approve) {
delete request;
payable(recipient).transfer(amount);
status = "Sent";
}
else if (
request.stillRequired == 0 ||
request.stillRequired == 1 && !approve &&
(approvals == 3 || recipient == request.sig1)
) {
delete request;
status = "Rejected";
}
else if (approve) {
request.sig2 = msg.sender;
status = "Pending";
}
else {
request.rejected = msg.sender;
status = "Rejected / Still Pending";
}
emit Transfer(status, request.stillRequired, msg.sender, recipient, amount);
}
}