|
pragma solidity 0.8.7; |
|
|
|
import "./ownable.sol"; |
|
|
|
contract MultiSigWallet is Ownable { |
|
//// TYPE DECLARATIONS //// |
|
struct Signature { |
|
User Owner; |
|
bool Signed; |
|
} |
|
struct Request { |
|
uint256 ID; |
|
User Owner; |
|
uint256 Amount; |
|
address ToAddress; |
|
string Status; |
|
Signature[] Signatures; |
|
} |
|
struct RequestsLookup { |
|
uint256[] Pending; |
|
uint256[] Completed; |
|
// TODO: can add more logic to account for Transfer Requests that get |
|
// denied. (chris) |
|
// uint256[] Denied; |
|
} |
|
|
|
//// STATE VARIABLES //// |
|
uint256 public requiredApprovals; |
|
Request[] private transferRequests; |
|
RequestsLookup private requestLog; |
|
string private _pendingStatus = "pending"; |
|
string private _completedStatus = "completed"; |
|
|
|
// constructor sets the owner address in OwnerList. |
|
constructor(address[] memory _owners, uint8 _approvalCount) { |
|
requiredApprovals = _approvalCount; |
|
// Loop through all addresses to add to OwnerList: |
|
for (uint256 i = 0; i < _owners.length; i++) { |
|
ownerList[_owners[i]] = User(_owners[i], true); |
|
} |
|
} |
|
|
|
//// EVENTS //// |
|
event Deposited(uint256 _amount, address indexed _fromAddress); |
|
event RequestCreated( |
|
uint256 _requestID, |
|
uint256 _amount, |
|
User indexed _owner, |
|
address indexed _toAddress |
|
); |
|
event RequestApproved(uint256 _requestID, address indexed _signedBy); |
|
event RequestCompleted(uint256 _requestID); |
|
|
|
//// MODIFIERS //// |
|
|
|
// canApproveRequest requests that the Transfer Request was found; that the |
|
// Contract has enough balance for the Transfer; that the signer isn't |
|
// trying to self-sign their own Transfer Request; and that the signer |
|
// hasn't already signed the Transfer Request. |
|
modifier canApproveRequest(uint256 _requestID) { |
|
Request memory request = transferRequests[_requestID]; |
|
require(request.Amount != 0, "Transfer Request not found"); |
|
require(request.Amount >= address(this).balance, "Insufficient funds"); |
|
require( |
|
msg.sender != request.Owner.Address, |
|
"Cannot self-sign Transfer Requests" |
|
); |
|
bool alreadySigned = false; |
|
for (uint256 i = 0; i < request.Signatures.length; i++) { |
|
if (msg.sender == request.Signatures[i].Owner.Address) { |
|
alreadySigned = true; |
|
break; // exit the loop early |
|
} |
|
} |
|
require(!alreadySigned, "Already signed this Transfer Request"); |
|
_; |
|
} |
|
|
|
//// PUBLIC FUNCTIONS //// |
|
|
|
// getBalance returns the Contracts balance. |
|
function getBalance() public view returns (uint256) { |
|
return address(this).balance; |
|
} |
|
|
|
// deposit transfers funds to the Contract (anyone can deposit funds). |
|
function deposit() public payable { |
|
emit Deposited(msg.value, msg.sender); |
|
} |
|
|
|
// createRequest will push a new Request to the TransferRequests array if |
|
// msg.sender is an Owner, and if _amount is greater than 0 and less than or |
|
// equal to the contracts balance. |
|
function createRequest(uint256 _amount, address _toAddress) public isOwner { |
|
require(_amount > 0, "Amount must be greater than 0"); |
|
require( |
|
address(this).balance >= _amount, |
|
"Amount must be <= contract balance" |
|
); |
|
|
|
// NOTE: cannot copy a "memory" type Signature[] to a new Request that |
|
// is stored in "storage", see error: |
|
// - UnimplementedFeatureError: Copying of type struct |
|
// MultiSigWallet.Signature memory[] memory to storage not yet |
|
// supported. |
|
// uint256 requestID = transferRequests.length; |
|
// Signature[] memory signatures; |
|
// transferRequests.push( |
|
// Request( |
|
// requestID, |
|
// _getOwner(), |
|
// _amount, |
|
// _toAddress, |
|
// _pendingStatus, |
|
// signatures |
|
// ) |
|
// ); |
|
// |
|
// Here's a workaround for this problem: |
|
// source: https://github.com/ethereum/solidity/issues/4115#issuecomment-612309066 |
|
transferRequests.push(); |
|
uint256 requestID = transferRequests.length - 1; |
|
Request storage newRequest = transferRequests[requestID]; |
|
newRequest.ID = requestID; |
|
newRequest.Owner = _getOwner(); |
|
newRequest.Amount = _amount; |
|
newRequest.ToAddress = _toAddress; |
|
newRequest.Status = _pendingStatus; |
|
|
|
requestLog.Pending.push(requestID); |
|
emit RequestCreated(requestID, _amount, _getOwner(), _toAddress); |
|
} |
|
|
|
// pendingRequests returns all un-completed Transfer Requests if msg.sender |
|
// is an Owner. |
|
function pendingRequests() public view isOwner returns (Request[] memory) { |
|
// NOTE: unable to do "requests[0] = request;" in loop unless we set a |
|
// length. |
|
// solution source: https://stackoverflow.com/a/55532934/1180523 |
|
Request[] memory requests = new Request[](requestLog.Pending.length); |
|
// Loop through the Pending request ID's and return each Transfer |
|
// Request for the ID's found: |
|
for (uint256 i = 0; i < requestLog.Pending.length; i++) { |
|
// NOTE: ".push()" cannot be used due for memory arrays: |
|
// - Member "push" is not available in struct Request memory[] |
|
// memory outside of storage. |
|
// docs info: https://docs.soliditylang.org/en/v0.8.10/types.html#allocating-memory-arrays |
|
// BUT chaning requests to use storage also doesn't work: |
|
// - This variable is of storage pointer type and can be accessed |
|
// without prior assignment, which would lead to undefined |
|
// behaviour. |
|
// |
|
// This is a workaround, I'm not sure if there's a better approach: |
|
// requests.push(r); |
|
uint256 requestID = requestLog.Pending[i]; |
|
requests[i] = transferRequests[requestID]; |
|
} |
|
return requests; |
|
} |
|
|
|
// completedRequests returns all completed Transfer Requests, this is |
|
// available to Owners and non-Owners. |
|
function completedRequests() public view returns (Request[] memory) { |
|
// NOTE: unable to do "requests[0] = request;" in loop unless we set a |
|
// length. |
|
// solution source: https://stackoverflow.com/a/55532934/1180523 |
|
Request[] memory requests = new Request[](requestLog.Completed.length); |
|
// Loop through the Completed request ID's and return each Transfer |
|
// Request for the ID's found: |
|
for (uint256 i = 0; i < requestLog.Completed.length; i++) { |
|
uint256 requestID = requestLog.Completed[i]; |
|
requests[i] = transferRequests[requestID]; |
|
} |
|
return requests; |
|
} |
|
|
|
// approveRequest adds an Approved signature to a Request if isOwner and not |
|
// self-signing the Request; Then if all required approvals are met, the |
|
// Request is moved from Pending to Completed status and the funds are |
|
// transferred. |
|
function approveRequest(uint256 _requestID) |
|
public |
|
isOwner |
|
canApproveRequest(_requestID) |
|
{ |
|
Request storage request = transferRequests[_requestID]; |
|
request.Signatures.push(Signature(_getOwner(), true)); |
|
emit RequestApproved(_requestID, _getOwner().Address); |
|
|
|
// if we have enough signatures, mark the request as complete and |
|
// transfer the funds: |
|
if (request.Signatures.length >= requiredApprovals) { |
|
_pendingToCompleted(request); |
|
payable(request.ToAddress).transfer(request.Amount); |
|
emit RequestCompleted(_requestID); |
|
} |
|
} |
|
|
|
//// PRIVATE FUNCTIONS //// |
|
|
|
// _getOwner returns msg.sender as a User{} struct type if the msg.sender is |
|
// an Owner. |
|
function _getOwner() private view isOwner returns (User memory) { |
|
return User(msg.sender, true); |
|
} |
|
|
|
// _pendingToCompleted moves the _request.ID out of RequestLog.Pending to |
|
// RequestLog.Completed and marks _request.Status as completed. |
|
function _pendingToCompleted(Request storage _request) private isOwner { |
|
uint256 pendingLength = requestLog.Pending.length; |
|
uint256 completedLength = requestLog.Completed.length; |
|
string memory previousStatus = _request.Status; |
|
uint256[] memory newPending; |
|
|
|
// Move requestID out of Pending to Completed TransferRequests: |
|
uint256 j = 0; |
|
for (uint256 i = 0; i < pendingLength; i++) { |
|
if (requestLog.Pending[i] != _request.ID) { |
|
newPending[j] = requestLog.Pending[i]; |
|
j++; |
|
} |
|
} |
|
requestLog.Pending = newPending; |
|
requestLog.Completed.push(_request.ID); |
|
_request.Status = _completedStatus; |
|
|
|
assert(requestLog.Pending.length == pendingLength - 1); |
|
assert(requestLog.Completed.length == completedLength + 1); |
|
// to compare strings, convert them to bytes, generate a Hashes, and |
|
// compare the hashes: |
|
// https://docs.soliditylang.org/en/v0.8.10/types.html#bytes-and-string-as-arrays |
|
assert( |
|
keccak256(bytes(_request.Status)) != |
|
keccak256(bytes(previousStatus)) |
|
); |
|
} |
|
} |