@mcgrane5 - I feel that the approvals should go inside the TransferRequest struct because they are very relevant to the state of the TransferRequest. For the initial version of the assignment I was not able to accomplish this because a mapping cannot exist inside a struct in storage so I had to resort to a hack by storing the approvals in a separate storage array. The array indexes of the requests array and the approvals array lined up 1:1 so I got away with it.
For the second version (presented below) I determined that an array can be part of a struct in storage but it seems a bit tricky to initialize such a struct - see my comment in createTransferRequest().
pragma solidity 0.7.5;
import ā./Ownable.solā;
contract MultiSigWallet is Ownable
{
enum RequestState {UNKNOWN, PENDING, SENT, CANCELLED }struct TransferRequest{ uint id; uint amount; address destination; RequestState state; uint requiredNumberOfApprovals; uint currentNumberOfApprovals; address[] addressesOfApproversThatHaventApproved; } TransferRequest[] transferRequests; uint defaultRequiredNumberOfApprovals; // this is a list of addresses that is used as a template for valid approvers when creating new transferRequests address[] defaultApprovalAddresses; event transferred(uint requestid, uint amount, address addr); event approvedBySignatory(uint requestid, address addr, uint currentNumberOfApprovals); function setRequiredNumberOfApprovals(uint _numApprovals) public onlyOwner { defaultRequiredNumberOfApprovals = _numApprovals; } function getRequiredNumberOfApprovals() public view onlyOwner returns(uint) { return defaultRequiredNumberOfApprovals; } function setApprovalAddresses( address[] memory _addresses) public onlyOwner { defaultApprovalAddresses = _addresses; } function getApprovalAddresses() public view onlyOwner returns(address[] memory) { return defaultApprovalAddresses; } // attempts to cancel the request while it is in PENDING state and returns true if the request is in cancelled state at the end of the operation function cancelTransferRequest(uint _requestId) public onlyOwner returns(bool) { TransferRequest storage theRequest = transferRequests[_requestId]; if( theRequest.state == RequestState.PENDING ) { theRequest.state = RequestState.CANCELLED; } return theRequest.state == RequestState.CANCELLED; } // I want this to return the new balance function deposit() public payable returns (uint) { // I wonder if this returns the balance before or after the deposit is complete... return address(this).balance; } // returns the requestId of the newly created request function createTransferRequest(uint _amount, address _destination) public returns (uint) { uint newRequestId = transferRequests.length; // Create the request in memory, then move to storage because // when I create it directly in storage, it wants me to // initialize addressesOfApproversThatHaventApproved but // I can't figure out how to create an empty address[] on storage TransferRequest memory newRequest; transferRequests.push( newRequest ); TransferRequest storage newRequestStorage = transferRequests[newRequestId]; newRequestStorage.id = newRequestId; newRequestStorage.amount = _amount; newRequestStorage.destination = _destination; newRequestStorage.state = RequestState.PENDING; newRequestStorage.requiredNumberOfApprovals = defaultRequiredNumberOfApprovals; newRequestStorage.currentNumberOfApprovals = 0; // set up all of the addresses that can approve this request in remainingApprovalAddresses bool senderIsOneOfDefaultAddresses; for(uint i = 0; i < defaultApprovalAddresses.length; i++) { address thisAddress = defaultApprovalAddresses[i]; newRequestStorage.addressesOfApproversThatHaventApproved.push(thisAddress); if(thisAddress == msg.sender) senderIsOneOfDefaultAddresses = true; } require(senderIsOneOfDefaultAddresses, "This Sender is not allowed to create requests."); return newRequest.id; } // notes the approval and sends the amount if the required number of approvals is reached // returns the state of the request after the function is complete function approveAndSend(uint _requestId) public returns (RequestState) { require(_requestId < transferRequests.length); TransferRequest storage theRequest = transferRequests[_requestId]; if( theRequest.state == RequestState.PENDING ) { // it's possible to approve even if there's not enough money in the contract, but it won't be sent and the approval will be rolled back for(uint i = 0; i < theRequest.addressesOfApproversThatHaventApproved.length; i++) { if(theRequest.addressesOfApproversThatHaventApproved[i] == msg.sender) { // approved by an approver that has not yet approved this request theRequest.currentNumberOfApprovals += 1; delete theRequest.addressesOfApproversThatHaventApproved[i]; // mark this approver down as having approved this request so that this approver doesn't get to approve multiple times emit approvedBySignatory(theRequest.id, msg.sender, theRequest.currentNumberOfApprovals); } } } if( theRequest.currentNumberOfApprovals >= theRequest.requiredNumberOfApprovals ) { theRequest.state = RequestState.SENT; payable(theRequest.destination).transfer(theRequest.amount); emit transferred(theRequest.id, theRequest.amount, theRequest.destination); } return theRequest.state; }
}