Project - Multisig Wallet

This is a question about loading the contract with Ethereum.

According to what I’ve read, a contract has to add these special functions to receive ether:

receive() external payable {}

fallback() external payable {}

But when I add those two functions, Remix doesn’t expose them in the UI for me to call. I can’t send ether to the contract this way. I had to add my own “deposit” function. Why did I need to do this? Why doesn’t remix expose those two functions to call from the User Interface?

1 Like

@Placebo you need to make a deposit function if you want to be able to send ether “into” your contract and use msg.value to specify the amount you want to send. The deposit function also needs to be payable. However if you want to send ether into a smart contract from another contract you can also will need to write a function called transfer to do this something like this

function transfer(address payable receiverAddress, uint256 amount) public {
    
    require(balance[msg.sender] >= amount, "insufficent funds");
    require(amount >= 0, "cannot send 0 amount");

    balance[msg.sender] -= amount;
    receiverAddress.transfer(amount);
}
1 Like

@Placebo. Heres a good post for you to read on the two functions you mentioned for extra reading
https://stackoverflow.com/questions/69178874/solidity-v0-6-0-fallback-functions-what-are-they-needed-for

Note: Since this contract doesn’t keep separate accounts, I just use the built-in balance function of the contract address(this).balance itself to manage how much money is available.

Wallet.sol

pragma solidity >=0.8.7;
//pragma abicoder v2;

// SPDX-License-Identifier: asdf

import “./Lockable.sol”;

contract Wallet is Lockable{

address[] Owners;

uint Required;

struct Transfer {
    address towhom;
    uint howmuch;
    uint whenSent;
    address[] approvers;
}

Transfer[] Transfers;

modifier onlyOwners() {
    require(FindAddress(msg.sender, Owners), "Only an owner can do this.");
    _;
}

//– 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. 
constructor(uint _required_approvals, address[] memory _owners) {
    Owners = _owners;
    Required = _required_approvals;
}

//– Anyone of the owners should be able to create a transfer request. 
// The creator of the transfer request will specify what amount 
// and to what address the transfer will be made.
function InitiateTransfer(address _towhom, uint _howmuch) public onlyOwners returns(uint) {
    require(address(this).balance >= _howmuch, "Not enough ether to send the requested amount");
    address[] memory a;
    Transfers.push(Transfer(_towhom, _howmuch, 0, a));
    //optional if Initiator should be implictly considered an approval, for now, no. 
    //Transfers[Transfers.length - 1].approvers.push(msg.sender);
    return Transfers.length - 1;
}

// because mappings can't be nested in structs, need to simulate a mapping using an array
function FindAddress(address _find, address[] storage _addresses) private view returns(bool) {
    for (uint id = 0; id < _addresses.length; id++) {
        if (_find == _addresses[id]) {
            return true;
        }
    }
    return false;
}

//– Owners should be able to approve transfer requests.
function Approve(uint _id) public onlyOwners lockable returns (uint) {
    require(FindAddress(msg.sender, Transfers[_id].approvers) == false, "You cannot approve more than once.");
    Transfers[_id].approvers.push(msg.sender); // new approver? Add to approver list for that transfer
    if (Transfers[_id].approvers.length < Required) {
        return Transfers[_id].approvers.length;  // don't send yet, tell caller how many approvals exist currently
    }
    require(address(this).balance >= Transfers[_id].howmuch, "Wallet does not have enough to send this.");
    
    // check for already spent and simultaneously set "already sent" (non zero whenSent)
    // another way to block multiple callers though there is a slight slot where this could happen
    // if ++ is not atomic in the EVM.
    require(Transfers[_id].whenSent++ == 0, "Transaction has already been sent.");
    Transfers[_id].whenSent = block.timestamp; // prevent recursive exploit (towhom calling back to Approve)
    
    //– When a transfer request has the required approvals, the transfer should be sent. 
    // if this hangs, the Lock (see lockable modifier) will stay in place forever???
    (bool sent, bytes memory data) = Transfers[_id].towhom.call{value: Transfers[_id].howmuch}("");
    
    if (sent == false) {
        // if the send failed, fix up the whenSent to be not-sent yet
        Transfers[_id].whenSent = 0;
        Transfers[_id].approvers.pop(); // remove the last approver so he can retry
        require(sent, "Transfer failed at receiving end");
    }

    return Transfers[_id].approvers.length;
}

function AddOwner(address _newowner) public onlyOwners {
    require(FindAddress(_newowner, Owners) == false, "Already an owner");
    Owners.push(_newowner); 
}

//– Anyone should be able to deposit ether into the smart contract
function deposit() payable public returns (uint){
    return GetBalance();
}

function GetBalance() public view returns (uint) {
    return address(this).balance;
}
}

Lockable.sol

pragma solidity >=0.8.7;
//pragma abicoder v2;

// SPDX-License-Identifier: asdf

// Mutual Exclusion - Prevent multiple callers inside a function at the same time
contract Lockable {
uint Locked;

constructor() {
    Locked = 0;
}

modifier lockable() {
    if (Locked++ > 0) {
        require(false, "Busy, try again");
    } 
    _;

    Locked = 0;
}
1 Like

A master piece from the most detailed and incredible teacher we have!

Love all the descriptions which make easier to understand what each function does :nerd_face:

Big hug to you Big J!

1 Like

A second version of my original solution after watching the first support video.

Modifications:

  • Added the double mapping to store each authorised signatory’s approvals of transfer requests. I no longer record the addresses, as they approve each request, within each Request instance.

  • Transfer requests can now be created while others are still pending. Each newly created transfer request is added to a requests array, where they remain stored even after having been sent or fully rejected. Two Boolean properties have been added to the Request struct to record whether each transfer request is eventually sent or fully rejected, so that this information is retained for transfers which are no longer pending.

  • Each transfer request is now given an ID number when it’s created:
     ID number = index number of Request instance in requests array + 1
    The IDs are stored as a separate property in each Request instance.

  • Each authorised signatory can call the forMyApproval() function to retrieve an array of all the pending transfer request IDs which they are entitled to approve. They can then call the getTransfer() function or the getTransferDetails function with a specific ID from this array in order to retrieve its data, and then proceed to call the approveOrReject() function with the same ID to either approve or reject that specific transfer request.
    The forMyApproval array() iterates over the array of stored transfer requests and adds each qualifying ID to a fixed-size memory array. The fixed size of the array is a variable which is determined for each authorised signatory by referencing the total number of pending transfer requests they are currently entitled to approve, and which is tracked in the pending mapping. The totals in this mapping are increased after each transfer request is created, and then reduced for each additional approval or rejection, all according to the specific parameters of each transfer request. Fixing the size of the array of IDs to this variable allows the loop to be exited, and the array returned, as soon as all of the relevant IDs have been found.
    Requests that are currently pending will be located towards the end of the requests array, and so by iterating in reverse, more and more unnecessary iterations will potentially be avoided as the total number of instances stored in the requests array continues to increase over time.
    Retrieving a smaller array of IDs in this way should end up being much more manageable than returning the whole array of Request instances, which will contain an ever-larger amount of unnecessary data.

  • Authorised signatories can only be changed by the contract creator at a time when there are no pending transfer requests. If this constraint wasn’t applied, some of the approvals recorded in the double mapping would no longer correspond to currently authorised signatories.

  • The total number of approvals required to send a transfer (2/3 or 3/3) is now recorded as a separate property within each Request instance, as well as the number of approvals still required. This is so that the total can be changed at any time, including when there are transfer requests pending. The new total will apply to all new transfer requests created after the change, whereas all requests that were pending at the time of the change will continue to require the same total number of approvals that were required at the time they were originally created.

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_B.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;

// Records how many transfer requests are awaiting each authorised signatory's approval    
   mapping(address => uint8) pending;

   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(
         pending[auth[0]] + pending[auth[1]] + pending[auth[2]] == 0,
         "To replace an authorised signatory there must be no pending transfer requests"
      );
      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_B

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
pragma abicoder v2;
import "./approval_B.sol";

contract MultisigWallet is Approval {
    
   constructor(uint8 _approvals, address _auth1, address _auth2, address _auth3)
   Approval(_approvals, _auth1, _auth2, _auth3) {}
    
   struct Request {
      uint id;
      address beneficiary;
      uint256 amount;
      uint8 totalApprovals; /* Need to record total approvals required within
      Request struct instance in case this is changed before transfer sent */
      uint8 stillRequired;  // Number of approvals still required
      bool sent;
      bool rejected;
      address notSignedBy; /* Records signatory if they reject a transfer request but it
      remains "Pending". This can only occur where 2 out of 3 approvals are required */
   }
    
// Log of all transfers: pending, sent and rejected
   Request[] private requests;

// Records which transfer requests have been approved by which authorised signatories 
   mapping(address => mapping(uint => bool)) private approvalTracker;

   modifier IdValid(uint256 id) {
      require(id != 0 && id <= requests.length, "Invalid ID");
      _;
   }

   event Transfer(
      string status, uint8 stillRequired, address auth, address beneficiary, uint256 amount
   );
    
   function getBalance() external view returns(uint256) {
      return address(this).balance;
   }

   function getPending() external view onlyAuth returns(uint256) {
      return pending[msg.sender];
   }
    
   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 {
   /* Check performed when transfer request created, in order to prevent
      unreasonable transfer amounts being requested, and to prevent such
      transfer requests from going through the approval process unnecessarily
      and wasting storage in the requests array.
      The transfer method will throw an error and revert the approveOrReject() 
      function if it eventually attempts to send a transfer when there is
      insufficient contract balance to cover it. */
      require(address(this).balance >= _amount, "Insufficient funds");

      Request memory request;
      request.id = requests.length + 1;       
      request.beneficiary = _beneficiary;
      request.amount = _amount;
      request.totalApprovals = approvals;

   /* Helper function establishes the number of approvals
      still required for this transfer to be sent */
      request.stillRequired = _furtherApprovals(_beneficiary, request.totalApprovals); 

      requests.push(request);
      approvalTracker[msg.sender][requests.length] = true;
   
   /* Helper function adds 1 to the pending count of each authorised signatory
      entitled to add their approval to this newly created transfer request */  
      _addPendingApproval(_beneficiary);

      emit Transfer("Pending", request.stillRequired, msg.sender, _beneficiary, _amount);
   }

/* Authorised signatories can retrieve an array of pending transfer
   request IDs which they are entitled to approve or reject */
   function forMyApproval() external view onlyAuth returns(uint256[] memory) {
      uint256[] memory myIds = new uint256[](pending[msg.sender]);
      uint8 counter;
      for (uint256 i = requests.length - 1; counter != pending[msg.sender]; i--) {
         if (
            !requests[i].sent && !requests[i].rejected &&
             requests[i].beneficiary != msg.sender &&
            !approvalTracker[msg.sender][i+1]
         ) {
            myIds[counter] = i + 1;
            counter++;
         }
      }
      return myIds;
   }

/* Any address can get details of any transfer by its ID, and whether
   transfer is pending, sent or rejected: RETURNS STRUCT INSTANCE */
   function getTransfer(uint256 id) external view IdValid(id) returns(Request memory requestData) {
      return requests[id-1];
   }

/* Any address can get details of any transfer by its ID, and whether
   transfer is pending, sent or rejected: RETURNS SEPARATE VALUES */
   function getTransferDetails(uint256 id) external view IdValid(id) returns(
      string memory status, address receiver, uint256 amount,
      uint256 approvalsRequired, address rejectedBy
   ) {
      Request memory request = requests[id-1];
      string memory _status;
      if (request.sent) _status = "Sent";
      else if (request.rejected) _status = "Rejected";
      else _status = "Pending";
      return (
         _status, request.beneficiary, request.amount,
         request.stillRequired, request.notSignedBy
      );
   }

// Other authorised signatories approve or reject a pending transfer request 
// Boolean parameter: true (approve/sign), false (reject/don't sign) 
   function approveOrReject(uint256 id, bool approve) external onlyAuth IdValid(id) {
      Request storage request = requests[id-1];
      require(!request.sent, "This transfer has already been sent");
      require(!request.rejected, "This transfer request has already been rejected");
      require(request.notSignedBy != msg.sender, "You have already rejected this transfer request");
      address recipient = request.beneficiary;
      require(
         recipient != msg.sender,
         "You cannot approve or reject this transfer request, because you are the beneficiary"
      );
      require(!approvalTracker[msg.sender][id], "You have already approved this transfer request");

      if (
         request.totalApprovals == 3 || approve || request.notSignedBy != address(0) ||
         recipient == auth[0] || recipient == auth[1] || recipient == auth[2]
      ) {
         request.stillRequired--;   
      }

      pending[msg.sender] -= 1;
      string memory status;

      if (request.stillRequired == 0 && approve) {
         approvalTracker[msg.sender][id] = true;
         request.sent = true;

         if (request.notSignedBy == address(0)) {
         /* Helper function subtracts 1 from the pending count of each
            authorised signatory who, due to the transfer now being sent,
            is no longer entitled to add their approval to this request */ 
            _removePendingApproval(id, recipient);
         }

         payable(recipient).transfer(request.amount);
         status = "Sent";
      }
      else if (
         request.stillRequired == 0 ||
         request.stillRequired == 1 && !approve &&
         (request.totalApprovals == 3 || approvalTracker[recipient][id]) 
      ) {
         request.rejected = true;
         request.stillRequired = 0;

         if (request.notSignedBy == address(0)) {
         /* Helper function subtracts 1 from the pending count of each
            authorised signatory who, due to the transfer now being rejected,
            is no longer entitled to add their approval to this request */
            _removePendingApproval(id, recipient);
         }

         status = "Rejected";
      }
      else if (approve) {
         approvalTracker[msg.sender][id] = true;
         status = "Pending";
      }
      else {
         request.notSignedBy = msg.sender;
         status = "Rejected / Still Pending";
      }

      emit Transfer(status, request.stillRequired, msg.sender, recipient, request.amount);
   }

/* Helper function adds 1 to the pending count of each authorised signatory
   entitled to add their approval to a newly created transfer request */
   function _addPendingApproval(address recipient) private {
      for (uint8 i = 0; i <= 2; i++) {
         if (msg.sender != auth[i] && recipient != auth[i]) {
            pending[auth[i]] += 1;
         }
      }
   }

/* Helper function subtracts 1 from the pending count of each authorised
   signatory who, due to a transfer being either sent or rejected,
   is no longer entitled to add their approval to the request */
   function _removePendingApproval(uint256 id, address recipient) private {
      for (uint8 i = 0; i <= 2; i++) {
         if (
            msg.sender != auth[i] && recipient != auth[i] &&
            !approvalTracker[auth[i]][id]
         ) {
            pending[auth[i]] -= 1;
         }
      }
   }

// Establishes number of approvals still required to send a transfer
/* If 2/3 approvals required, and creator of new request is also
   the beneficiary, then 2 further approvals still required */
/* 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 */
   function _furtherApprovals(
      address recipient, uint8 totalApprovals
   ) private view returns(uint8) {
      return totalApprovals == 2 && recipient != msg.sender ||  
      totalApprovals == 3 && recipient != msg.sender &&
      (recipient == auth[0] || recipient == auth[1] || recipient == auth[2])
      ? 1 : 2;
   }

}

@thecil, @mcgrane5

1 Like

Another version of the project, this time based on the template provided in the support video.

Additional functionality I’ve included, which isn’t in the template …

  • TransferData event, which is emitted when a new transfer request is created, and on each additional approval. The data logged by this event is:
    (1) ID of transfer request
    (2) Staus of transfer request: either Pending (if further approvals are required before it can be sent) or Sent
    (3) Transfer amount
    (4) Address of the beneficiary/receiver
    (5) Owner address approving the transfer request
    (6) Number of approvals still required before the limit is reached and the transfer can be sent (zero if the event is logging a sent transfer).

  • I have applied a minimum and maximum to the number of required approvals input as the _limit argument on deployment.
    The minimum of 2 is to ensure that it is a multisig wallet.
    The maximum = total owners - 1; this is to ensure an owner cannot approve a transfer if they are also the receiver. If the creator of a transfer request is also the receiver, then the transaction which creates such a request doesn’t count towards its number of approvals (as it does in other cases). Once created, an owner is prevented from adding their approval to any transfer request if they are also the receiver.

  • Having the min/max constraint on the number of required approvals means that there must also be a minimum of 3 owner addresses input within the _owners array argument on deployment, and each address must also be different, however many are input. No maximum number of owner addresses needs to be applied.
    In order to check that each owner address is different, a helper function is used to iterate over the _owners array input into the constructor. A double loop is used so that a varying number of owner addresses can be checked.

  • Another helper function is called from within the require statement in the onlyOwners modifier. This helper function iterates over the array of owner addresses stored in the owners state variable. Iterating over the array enables an address calling a function to be compared with all of the owner addresses, however many there are, to ensure that it matches one of them.

  • getBalance function to retrieve the current contract balance. As the contract is a multisig wallet, the contract balance is the same as the wallet balance.

One disadvantage I can see with this particular solution is that if an owner decides not to approve a transfer request, then there is no option for them to explicitly reject it. This means that if two owners decide not to approve the same transfer request it will remain “Pending” indefinately; and this will also happen when only one owner decides not to approve a transfer request if the receiver is one of the other owners. My previous solution (in the post above) includes an option for an owner to register their rejection of a transfer request; and when such a rejection makes it impossible for a request to obtain the required number of approvals to be sent, the emitted event logs the transfer request as “Rejected”, and this definitive rejection is also recorded in the Boolean rejected property within the transfer request’s struct instance stored in the requests array.

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {

   address[] public owners;
   uint limit;

   struct Transfer {
      uint id;
      uint amount;
      address payable receiver;
      uint approvals;
      bool hasBeenSent;   
   }

   Transfer[] transferRequests;

   mapping(address => mapping(uint => bool)) approvals;

   event TransferData(
      uint id, string status, uint amount, address receiver,
      address ownerApproval, uint stillRequired
   );

   // Initializes the owners list and the approvals limit
   constructor(address[] memory _owners, uint _limit) {
/* 1) Minimum approval limit = 2, to ensure wallet is multisig.
   2) Maximum approval limit = total owners - 1, to allow an owner to
      be prevented from adding their approval to a transfer if they are
      also the receiver.
   3) All owner addresses must be different to allow owners to be
      prevented from approving the same transfer request more than once.
   4) Therefore, there must be a minimum of 3 owner addresses,
      but no maximum needs to be applied. */   
      require(_owners.length > 2, "There must be a minimum of 3 owners");
      require(
         _limit < _owners.length && _limit > 1,
         "Required approvals: min = 2; max = total owners - 1"
      );
      
      // Calls helper function
      require(_checkOwners(_owners), "Each owner must be a different address");

      limit = _limit;
      owners = _owners;
   }

   // Only allows people in the owners list to continue the execution.
   modifier onlyOwners {
      // Calls helper function
      require(_isOwner(), "Operation restricted to owners");
      _;
   }

   // Empty function
   function deposit() public payable {}

   /* Creates an instance of the Transfer struct and adds it to the
      transferRequests array */
   function createTransfer(
      uint _amount, address payable _receiver
   ) public onlyOwners {
      /* Check performed when transfer request created, in order to prevent
         unreasonable transfer amounts being requested, and to prevent
         such transfer requests from going through the approval process
         unnecessarily and wasting storage in the transferRequests array.
         The transfer method will throw an error and revert the approve() 
         function if it eventually attempts to send a transfer when there is
         insufficient contract balance to cover it. */
      require(address(this).balance >= _amount, "Insufficient funds");

      Transfer memory request;

      // The ID of a transfer request is based on its position in the array
      request.id = transferRequests.length + 1;

      request.amount = _amount;
      request.receiver = _receiver;
        
      /* If the creator of the request is also the receiver then their
         approval doesn't count towards the total required (the limit) */
      if (_receiver != msg.sender) request.approvals = 1;

      transferRequests.push(request);
      approvals[msg.sender][transferRequests.length] = true;

      emit TransferData(
         request.id, "Pending", request.amount, request.receiver, 
         msg.sender, limit - request.approvals
      );
   }

   // An owner can approve one of the transfer requests (by its ID)
   // The data recorded in the Transfer object (struct instance) is updated
   function approve(uint _id) public onlyOwners {
      require(_id != 0 && _id <= transferRequests.length, "Invalid ID");
      Transfer storage request = transferRequests[_id - 1];

      // An owner cannot approve a tranfer request that has already been sent.
      require(!request.hasBeenSent, "This transfer has already been sent");

      /* An owner cannot add their approval to a transfer request
         if they are also the receiver */
      require(
         request.receiver != msg.sender,
         "You're the receiver, so you cannot approve this transfer request"
      );

      // An owner cannot approve the same transfer request more than once
      require(
         !approvals[msg.sender][_id],
         "You have already approved this transfer request"
      );

      request.approvals++;
      string memory status;

      /* When the number of approvals for a transfer has reached the limit,
         the transfer is sent to the receiver */
      if (request.approvals == limit) {
         /* As the transfer is now being sent, there is no need to update
            the mapping to record the approval */
         // approvals[msg.sender][_id] = true;
         request.hasBeenSent = true;
         request.receiver.transfer(request.amount);
         status = "Sent";
      }
      else {
         /* If number of approvals hasn't reached limit yet, the mapping
            needs to be updated to record the approval of msg.sender */
         approvals[msg.sender][_id] = true;
         status = "Pending";
      }

      emit TransferData(
         _id, status, request.amount, request.receiver,
         msg.sender, limit - request.approvals
      );
   }

   // Returns all transfer requests
   function getTransferRequests() public view returns(Transfer[] memory) {
      return transferRequests;
   }

   function getBalance() public view returns(uint) {
      return address(this).balance;
   }

   /*** HELPER FUNCTIONS ***/

   // Iterates over array of owners to check that each is a different address
   // Nested loop enables varying numbers of owners to be checked
   function _checkOwners(address[] memory _owners) private pure returns(bool) {
      for (uint i = 0; i < _owners.length - 1; i++) {
         for (uint j = i + 1; j < _owners.length; j++) {
            if (_owners[i] == _owners[j]) return false;
         }
      }
      return true;
   }

   // Checks that the calling address is one of the owners
   // Iterating over the array enables varying numbers of owners to be checked
   function _isOwner() private view returns(bool) {
      for (uint i = 0; i < owners.length; i++) {
         if (msg.sender == owners[i]) return true;
      }
      return false;
   }

}
2 Likes

My multisig wallet has 3 contracts: Ownable, RequestAndApprove and MultisigWallet.

  • Ownable manages to check that only the owners of the wallet are the ones who can deposit, create transfer requests, and approve a request;
  • RequestAndApprove controls how a request is created and gets approved by owners;
  • MultisigWallet inhirits functions and state variables from Ownable and RequestAndApprove. It has a constructor which lets the wallet creator input the owners’ addresses and set the approval limit; the contract also has a couple of functions such as deposit, and get the balance of the wallet.

How it works

  • The contract creator can input an array of addresses to set the owners of the wallet contract and set the numbers of approvals required for a transfer in the constructor;
  • Any owner can create many transfer requests as long as the transferred amount is not larger than the balance of the wallet. All the transfer requests are stored in the array allRequests (of the contract RequestAndApprove);
  • Any owner can vote for the request and only is able to vote one time. After the request’s vote count is equal to or greater than the approval limit, the fund will be transferred to the recipient and the request will be disabled.

Contract codes

Ownable.sol

State variable owners stores all information of owners, includes:

  • owner’s address is mapped to
    depositAmount: the amount of eth they have deposited
    isOwner: modifier onlyOwners uses this attribute to check if a sender is a owner or not
// SPDX-License-Identifier: MIT

pragma solidity 0.7.5;
pragma abicoder v2;

contract Ownable {
    struct Owner {
        uint depositAmount;
        bool isOwner;
    }

    mapping(address => Owner) internal owners;

    modifier onlyOwners {
        require(owners[msg.sender].isOwner, 
                "Only owners can run this function");
        _;
    }

    function getOwner(address _ownerAddress)
    public view returns(Owner memory) {
        return owners[_ownerAddress];
    }
}

Request_And_Approve.sol

Each request transfer includes:

  • requestId: because the wallet lets many requests exist at the same time, the requestId helps owners choose which request they want to approve first;
  • requester: the address of the request creator;
  • recipient: the recipient;
  • amount: the amount to transfer;
  • description: to explain the purpose of a transfer;
  • voteCounts: the attribute keeps track of how many approvals a request gets, when the numbers of votes reach the approval limit, the amount will be transferred;
  • isActive: initially, it is true when a request is created:
    – if it is true, any owner can approve a request;
    – if it is false (when the fund is transferred), no one can vote the request anymore.

All requests created will be stored in an array named allRequests.

State variable approvers keeps track of owners’ approval activities to make sure no one can approve any request more than 1 time.

A request only get approved by function approve(uint _requestId, address _approver) internal if:

  • the one who executes the function should be one of the owners: this test is handled in the MultisigWallet contract, that’s why this function has the modifier internal;
  • require(_requestId < allRequests.length): check to make sure that a request exists;
  • require(currentRequest.isActive): only active requests can get approved;
  • require(!approvers[_approver][_requestId]): no one can vote any request more than 1 time;
  • require(currentRequest.amount <= _getBalance()): even though the function request already checks to make sure every request created with the amount being less than or equal to the wallet balance, but because the wallet accepts multiple requests existed at the same time, so that there is a case where some requests get approved first spending the wallet balance to a point which makes the wallet balance is not sufficient to some other active requests anymore, so that’s why the approve() function should run this test again.
// SPDX-License-Identifier: MIT

pragma solidity 0.7.5;
pragma abicoder v2;

contract RequestAndApprove {
    struct Request {
        uint requestId;
        address requester;
        address payable recipient;
        uint amount;
        string description;
        uint voteCounts;
        bool isActive;
    }
    Request[] public allRequests;

    mapping(address => mapping(uint => bool)) internal approvers;
    
    uint internal approvalLimit;
    
    event requestCreated(uint requestId, 
                            address requestor, 
                            address recipient, 
                            uint amount, 
                            string description);
    event approvalTriggered(address approver, 
                            uint voteCounts, 
                            bool isActive);

    function _getBalance() private view returns(uint) {
        return address(this).balance;
    }

    function request(address payable _recipient, 
                        uint _amount, 
                        string memory _description)
    internal {
        require(_amount <= _getBalance(),
                "The amount you enter is too large");

        uint id = allRequests.length;
        _request(id, msg.sender, _recipient, _amount, _description);

        emit requestCreated(id, 
                            msg.sender, 
                            _recipient, 
                            _amount, 
                            _description);
    }

    function _request(uint _requestId, 
                        address _requestor, 
                        address payable _recipient, 
                        uint _amount, 
                        string memory _description) 
    private {        
        Request memory newRequest = Request({
            requestId: _requestId,
            requester: _requestor,
            recipient: _recipient,
            amount: _amount,
            description: _description,
            voteCounts: 1,
            isActive: true
        });

        approvers[_requestor][_requestId] = true;
        allRequests.push(newRequest);
    }

    function approve(uint _requestId, address _approver) internal {
        require(_requestId < allRequests.length,
                "This request doesn't exist");

        Request storage currentRequest = allRequests[_requestId];

        require(currentRequest.isActive,
                "This request is already completed");
        require(!approvers[_approver][_requestId],
                "You already vote for this request");
        require(currentRequest.amount <= _getBalance(),
                "The amount of this request is too large.");

        _approve(_requestId, _approver);

        emit approvalTriggered(msg.sender, 
                                currentRequest.voteCounts, 
                                currentRequest.isActive);
    }

    function _approve(uint _requestId, address _approver) private {
        Request storage activeRequest = allRequests[_requestId];
        activeRequest.voteCounts += 1;
        approvers[_approver][_requestId] = true;

        if (activeRequest.voteCounts >= approvalLimit) {            
            activeRequest.isActive = false;
            activeRequest.recipient.transfer(activeRequest.amount);
        }
    }
}

Multisig_Wallet.sol

The wallet creator can input an array of addresses to define the owners of the wallet.
In the constructor, the private function _convertOwnersArrayToMapping will map every address to an initial value (with the deposit amount starts with 0; and set isOwner to true, so the modifier onlyOwners (of the Ownable contract can tell which addresses are the owners of the wallet) :

Owner memory _owner = Owner({
            depositAmount: 0,
            isOwner: true
        });

This contract also has a couple of functions: deposit() and getWalletBalance().

This contract inherits the request function, the approve function and the owners check from Ownable and RequestAndApprove.

// SPDX-License-Identifier: MIT

pragma solidity 0.7.5;
pragma abicoder v2;

import "./Ownable.sol";
import "./Request_And_Approve.sol";

contract MultisigWallet is Ownable, RequestAndApprove {
    event depositDone(address depositor, uint amount);

    constructor(address[] memory _owners, uint _approvalLimit) {
        require(_approvalLimit <= _owners.length,
                "Approval limit is too large");

        _convertOwnersArrayToMapping(_owners);
        approvalLimit = _approvalLimit;
    }

    function _convertOwnersArrayToMapping(address[] memory _ownersArray)
    private {
        Owner memory _owner = Owner({
            depositAmount: 0,
            isOwner: true
        });

        for (uint i = 0; i < _ownersArray.length; i++) {
            address ownerAddress = _ownersArray[i];
            owners[ownerAddress] = _owner;
        }
    }

    function deposit() public payable onlyOwners {
        require(msg.value > 0, 
                "The amount you enter should be greater than 0");

        owners[msg.sender].depositAmount += msg.value;

        emit depositDone(msg.sender, msg.value);
    }

    function getWalletBalance() 
    public view returns(uint) {
        return address(this).balance;
    }

    function requestTransfer(address payable _recipient, 
                        uint _amount, 
                        string memory _description)
    public onlyOwners {
        request(_recipient, _amount, _description);
    }

    function approveRequest(uint _requestId) 
    public onlyOwners {
        approve(_requestId, msg.sender);
    }
}
1 Like

Great assignment. This is my first version.
Although I already have lots of ideas about how to improve it, this is the assignment it was demanded. I am planning to post new versions here soon.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {

    address[] public owners;    // array of owners
    uint approvalsLimit;        // limit of approvals quantity

    struct Transfer{
        uint id;
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
    }
    Transfer[] transferRequests;
    mapping(address => mapping(uint => bool)) approvals;

    modifier onlyOwners {
        bool isOwner = false;

        for (uint i = 0; i < owners.length; i++){ 
            if (owners[i] == msg.sender) {
                isOwner = true;
                break;
            }
        }

        require(isOwner == true);
        _; //run function
    }

    event DepositDone(address owner, uint amount);
    event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _receiver);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);

    constructor(address[] memory _owners, uint _approvalsLimit) {
        require( _approvalsLimit <= _owners.length, "Error: approvals limit is greater than owners quantity.");
        owners = _owners;
        approvalsLimit = _approvalsLimit;
    }

    //Create an instance of the Transfer struct and add it to the transferRequests array
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        require(address(this).balance >= _amount, "Error: insufficient funds.");
        transferRequests.push ( Transfer( transferRequests.length, _amount, _receiver, 0, false) );

        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);
    }

    // Approves and sent the transfer if reached the limit
    function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] == false, "Error: owner already voted.");
        require(transferRequests[_id].hasBeenSent == false, "Error: transfer has already been sent.");
        require(address(this).balance >= transferRequests[_id].amount, "Error: insufficient funds.");

        approvals[msg.sender][_id] == true;
        transferRequests[_id].approvals++;

        emit ApprovalReceived(_id, transferRequests[_id].approvals, msg.sender);

        if ( transferRequests[_id].approvals >= approvalsLimit){
            transferRequests[_id].hasBeenSent == true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
            emit TransferApproved(_id);
        }
    }

    //Return all transfer requests
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    }

    // Deposit
    function deposit() public payable {
        emit DepositDone(msg.sender, msg.value);
    }
    
    // Get contract balance
    function getBalance() public view returns(uint) {
        return address(this).balance;
    }
    
    function selfDestroy() public onlyOwners {
        selfdestruct(msg.sender);
    }
}
1 Like

The following is my answer.

pragma solidity 0.7.5;
pragma abicoder v2;

contract MSWallet{

    address private owner;
    address private owner2;
    address private owner3;
    uint private appReq;

    event depositMade(uint amount, address depositor);
    event transferInitiated(address from, address to, uint amount, uint txID);
    event transferApproved(uint txID, address approver);
    event transferExecuted(address from, address to, uint amount, uint txID);
    

    modifier onlyOwner {
        require(msg.sender == owner || msg.sender == owner2 ||  msg.sender == owner3, "Address does not have admin access");
        _; // This underscore basically means "continue with running function"

    }

    constructor(address _owner2, address _owner3, uint _approval_Requirement) {
        require(msg.sender != _owner2 && msg.sender != _owner3 && _owner2 != _owner3, "Duplicate owner addresses");
        require(_approval_Requirement <= 3, "Approval requirement <= 3");
        owner = msg.sender;
        owner2 = _owner2;
        owner3 = _owner3;
        appReq = _approval_Requirement;
        
    }

    struct fundTransfer {
        address from;
        address payable to;
        uint amount;
        uint txId;
        uint approvals;
        address[3] approvalLog;
        bool isApproved;
    }

    fundTransfer[] fundTransferLog;

    function deposit() public payable {
        emit depositMade(msg.value, msg.sender);
        
    }

    function walletBalance() public view returns(uint) {
        return address(this).balance;
    
    }

    function initiateFundTransfer(uint _amount, address payable _recipient) public onlyOwner {
        require(address(this).balance >= _amount, "Insufficient Balance");
        address[3] memory _approvalLog;
        _approvalLog[0] = msg.sender;

        if (appReq > 1){
            emit transferInitiated(msg.sender, _recipient, _amount,fundTransferLog.length);
            fundTransferLog.push(fundTransfer(msg.sender, _recipient, _amount, fundTransferLog.length, 1, _approvalLog, false));

        } else {
            fundTransferLog.push(fundTransfer(msg.sender, _recipient, _amount, fundTransferLog.length, 1, _approvalLog, true));
            emit transferApproved(fundTransferLog.length, msg.sender);
            _sendFunds(_amount, _recipient);
            emit transferExecuted(msg.sender, _recipient, _amount, fundTransferLog.length);

        }
        
    }

    function approveFundTransfer(uint _txID) public onlyOwner {
        require(address(this).balance >= fundTransferLog[_txID].amount, "Insufficient balance.");
        require(fundTransferLog[_txID].isApproved != true, "Transaction has already been executed.");
        
        //Check if owner has already approved transaction.
        uint check = 0;
        for (uint i = 0; i < fundTransferLog[_txID].approvalLog.length; i ++){
            if (msg.sender == fundTransferLog[_txID].approvalLog[i]) {
                check ++;
            }

        }

        require(check == 0, "Owner has already approved transaction.");

        fundTransferLog[_txID].approvals += 1;
        uint appIndex = fundTransferLog[_txID].approvals - 1;
        fundTransferLog[_txID].approvalLog[appIndex] = msg.sender;
        emit transferApproved(_txID, msg.sender);
        
        if (fundTransferLog[_txID].approvals >= appReq) {
            fundTransferLog[_txID].isApproved = true;
            _sendFunds(fundTransferLog[_txID].amount, fundTransferLog[_txID].to);
            emit transferExecuted(fundTransferLog[_txID].from, fundTransferLog[_txID].to, fundTransferLog[_txID].amount, _txID);
        }

    }

    function _sendFunds(uint _amount, address payable _recipient) private returns (uint){
        _recipient.transfer(_amount);
        return address(this).balance;
    }

    function getTransaction(uint _txID) public view returns(fundTransfer memory) {
        return fundTransferLog[_txID];
    
    }

}
1 Like
// SPDX-License-Identifier: MIT
pragma solidity >= 0.7.0 < 0.9.0;
pragma abicoder v2;

contract Wallet {
    address[] public owners;
    uint limit;
    
    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approvals;
    
    //Should only allow people in the owners list to continue the execution. ✅
    modifier onlyOwners(){
        bool owner = false;
        for(uint i = 0; i < owners.length; i++){
            if(msg.sender == owners[i]){
                owner = true;
            }
        }
        require(owner, "Access Denied");
        _;
    }

    //Should initialize the owners list and the limit ✅
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    //Empty function ✅
    function deposit() public payable {}
    
    //Create an instance of the Transfer struct and add it to the transferRequests array ✅
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        Transfer memory  transferRequest = Transfer(_amount, _receiver, 0, false, transferRequests.length);
        transferRequests.push(transferRequest);
    }
    
    //Set your approval for one of the transfer requests. ✅
    //Need to update the Transfer object. ✅
    //Need to update the mapping to record the approval for the msg.sender. ✅
    //When the amount of approvals for a transfer has reached the limit, this function should send the transfer to the recipient. ✅
    //An owner should not be able to vote twice. ✅
    //An owner should not be able to vote on a tranfer request that has already been sent. ✅
    function approve(uint _id) public onlyOwners {
        require(!transferRequests[_id].hasBeenSent, "Transfer already approved");
        require(!approvals[msg.sender][_id], "Cannot vote twice");

        transferRequests[_id].approvals++;
        approvals[msg.sender][_id] = true;

        if(transferRequests[_id].approvals == limit){
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
            transferRequests[_id].hasBeenSent = true;
        }
    }
    
    //Should return all transfer requests
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    } 
}

so i kinda made something that goes along the lines of a smart contract that allows users to deposit with multiple signatories, allowing the user to choose how many other signatories are required to, allowing the owner and any of the signatories to request for transfer, allowing owner or signatories to approve the request until the minimum signatures are required before transfer occurs. Here is my very un-optimized code that’s also available on my GitHub: https://github.com/KKCheah/MultiSigWallet_Ethereum/blob/main/multisig_prototype.sol

pragma solidity 0.7.5;
pragma abicoder v2;
contract MultiSigWallet{
    
   
    //mapping
    mapping(address => mapping(uint => uint))depositID;
    mapping(address => mapping(uint => uint))balance;
    mapping(address => mapping(uint => address))depositOwner;
    mapping(address => mapping(uint => mapping(uint => uint)))WithdrawSettingID;
    mapping(address => mapping(uint => mapping(uint => uint)))TransferRequestID;
    
    //state variables?
    address[] walletOwners;
    address payable[] clearToAdd;
    address payable[] adr;
    address[] toAdd;
    bool[] toFalse;
    uint fillerInt = 0;
    
    
    //structs

    //struct to keep deposit records
    struct depositRecord {
       uint txnId;
       address depositOwner;
       uint numberOfOwners;
       uint numberOfApprovalsForTransfer;
       uint valueOfDeposit;
   }
   
   depositRecord[] InitialDeposit;
   
    //struct for setting transfer permission   
    struct transferPermission {
       uint depositId;
       address[] depositUsers;
       bool[] withdrawStatus;
       uint minNoOfApproval;
       uint WithdrawSettingID;
       bool settingComplete;
   }
   
   transferPermission[] transferPermissionSetting;
   
   //struct for storing transfer information
   struct withdrawalInfo {
       address userRequesting;
       uint depositID;
       uint WithdrawSettingID;
       uint TransferingNo;
       uint transferValue;
       address toTransfer;
       bool finalApproval;
   }
   
   withdrawalInfo[] withdrawalInformation;
   
   
   //event
   event SuccessfulTransfer(uint indexed value, address indexed recipient, uint indexed tranferRequestNo);
   
    //function to deposit Ether into this contract
    function depositEtherToContract(uint _totalOwners, uint _approvalsForTransfer) public payable returns (string memory, uint){
        require(msg.value > 0, "There is no Ether being deposited");
        assert(msg.value > 0);
        
        uint depositIndex = InitialDeposit.length;
        uint depositTxnID = InitialDeposit.length;
        InitialDeposit.push(depositRecord(depositID[msg.sender][depositIndex], msg.sender, _totalOwners, _approvalsForTransfer, msg.value));

        
        depositOwner[msg.sender][depositTxnID] = msg.sender; 
        balance[msg.sender][depositTxnID] = msg.value;
        depositID[msg.sender][depositTxnID] = depositTxnID;
        
        return ("Deposit success, please key in deposit transaction ID in the future to check deposit. Your Txn ID is", depositTxnID);
    }
    
    //function to check the deposit using the Txn ID
    function findDeposit(uint _txnId) public view returns (depositRecord memory) {
        uint depositNumber = depositID[msg.sender][_txnId];
        require (depositNumber >= 0, "You've key in an invalid number aka wrong transaction ID etc");
        return InitialDeposit[depositNumber];
    }
    
    
    //function to assign owner and subowner for specific deposit
    function assignSubOwners(uint _txnId, address _subOwner)public returns (string memory, uint){
        
        
        uint depositNumber  = depositID[msg.sender][_txnId];
        uint depositIndexAssign = depositNumber; //index starts at zero but content with length starts at 1
        uint noShareOwners = InitialDeposit[depositIndexAssign].numberOfOwners;
        uint _minNoOfApproval = InitialDeposit[depositIndexAssign].numberOfApprovalsForTransfer;
        require (msg.sender == InitialDeposit[depositIndexAssign].depositOwner);
        
        if (noShareOwners > toAdd.length){
        toAdd.push(_subOwner);
        toFalse.push(false);
        
        } 
        
        if (noShareOwners == toAdd.length) {
            uint transferSetupID = transferPermissionSetting.length;
            uint transferNumber = transferPermissionSetting.length;
            transferPermissionSetting.push(transferPermission(depositNumber, toAdd, toFalse, _minNoOfApproval, transferSetupID, true));
            WithdrawSettingID[msg.sender][_txnId][transferSetupID] = transferNumber;
            delete toAdd;
            delete toFalse;
            return ("A address has been added to approve withdrawals, All subOwners Filled. Your transfer Request ID is", transferSetupID) ;
        }
        
        return ("A address has been added to approve withdrawals", fillerInt);
        
    }
    
    //function to check status of withdrawal permission of specific depositNumber
        function checkPermissionSetting(uint _txnId, uint _transferSetupID)public view returns (transferPermission memory){
        uint transferNumber = WithdrawSettingID[msg.sender][_txnId][_transferSetupID];
        uint transferIndex = transferNumber;
        return transferPermissionSetting[transferIndex];
    }
    
    //function to request for withdrawal from any of the owner/subOwners
        function requestForTransfer(uint _depositID, uint _transferSetupID, address _mainOwner, uint _transferValue, address _recipient) public returns (string memory, uint){
        
        uint transferNumber = withdrawalInformation.length;
        uint requestNumber = WithdrawSettingID[_mainOwner][_depositID][_transferSetupID];
        bool scanStatus = false;
        uint requestIndex = requestNumber;
        
        
        for (uint i = 0; transferPermissionSetting[requestNumber].depositUsers.length >= i; i++){
            if (transferPermissionSetting[requestNumber].depositUsers[i] ==  msg.sender){
                scanStatus = true;
                transferPermissionSetting[requestNumber].withdrawStatus[i] = true;
                TransferRequestID[msg.sender][requestNumber][withdrawalInformation.length] = transferNumber;
                
                require(InitialDeposit[_depositID].valueOfDeposit >= _transferValue, "Insufficient funds to perform transfer");
                
                withdrawalInformation.push(withdrawalInfo(msg.sender, _depositID, requestIndex, transferNumber, _transferValue, _recipient, false));
                
                assert(InitialDeposit[_depositID].valueOfDeposit >= _transferValue);
                
                return ("Request accepted and pending, you transfer number is", transferNumber) ;
            }
        }
        
        if (scanStatus == false){
            return ("You are not verified for the transaction stated, please key in deposit and transfer setting ID again, error code", 1);
        } 
        
        return ("You're not supposed to reach here, error code:", 2);
    }
    
        function check(uint _transferNo) public view returns(withdrawalInfo memory){
            return withdrawalInformation[_transferNo];
        }
        
        
        function approveRequestForTransfer(address _requestUser, uint _transferNo) public returns (address, string memory) {
            if (withdrawalInformation[_transferNo].userRequesting == _requestUser){
                bool scanStatus = false;
                uint countApproval = 0;
                uint value = withdrawalInformation[_transferNo].WithdrawSettingID;
                uint arrayLength = transferPermissionSetting[value].depositUsers.length;
                for (uint z = 0; arrayLength >= z; z++){
                    if (transferPermissionSetting[value].depositUsers[z] ==  msg.sender){ 
                        scanStatus = true;
                        transferPermissionSetting[value].withdrawStatus[z] = true;
                        for (uint y = 0; transferPermissionSetting[value].withdrawStatus.length >= y; y++){
                            if (transferPermissionSetting[value].withdrawStatus[y] = true) {
                                countApproval++;
                                if(countApproval>=transferPermissionSetting[value].minNoOfApproval){
                                require (transferPermissionSetting[value].depositId == InitialDeposit[value].txnId, "Error at the comparison");
                                balanceTranfer(_transferNo, withdrawalInformation[_transferNo].transferValue);
                                resetWithdrawStatus(_transferNo);
                                return (msg.sender, "Congratulation");
                                }
                            }
                        }
                        return (msg.sender, "Approved");
                    }
                }
                return (msg.sender, "Failed 1");
            }
        return (msg.sender, "Failed 2");
        }
        
        function balanceTranfer(uint _transferNo, uint _valueOfTransfer ) private returns (string memory, address, string memory, uint){
            withdrawalInformation[_transferNo].finalApproval = true;
            payable(withdrawalInformation[_transferNo].toTransfer).transfer(_valueOfTransfer);
            InitialDeposit[withdrawalInformation[_transferNo].depositID].valueOfDeposit  -= _valueOfTransfer;
            emit SuccessfulTransfer(_valueOfTransfer, withdrawalInformation[_transferNo].toTransfer, _transferNo);
            return ("transferred to address: ", withdrawalInformation[_transferNo].toTransfer, " with a value of: ", _valueOfTransfer);
        }
        
        function resetWithdrawStatus(uint _transferNo) private {
            for (uint i = 0; transferPermissionSetting[withdrawalInformation[_transferNo].WithdrawSettingID].withdrawStatus.length <= i; i++){
                transferPermissionSetting[withdrawalInformation[_transferNo].WithdrawSettingID].withdrawStatus[i] = false;
            }
        }
}

Does Truffle have a “pager” (I couldn’t find anything using google). That is, if I display something and it’s larger than a page, is there a way to have it pause a screenful at a time? Like more? or less?

P.S. one solution is for me to use a terminal app that lets me scroll text that has gone off screen. I’m used to using tmux or screen where the scrolling is bit clumsy. I can work around that.

Find below my code for this assignment:

pragma solidity 0.7.5;
pragma abicoder v2;

contract mWallet {

    struct Transfer{
        uint index;
        address payable receiver;
        uint amount;
        uint numberOfApprovals;
        bool transferSent;
    }

    event TransferRequestCreated(uint _id, uint _amount, address indexed _initiator, address indexed _receiver);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);

    Transfer[] transferRequests;
    address[] public addressOwners;
    uint public numberApprovers;

    mapping(address => uint) myBalance;
    mapping(address => mapping(uint => bool)) approvalOfTransfers;

    event depositDone(uint amount, address indexed sender);
    event balanceTransfered(uint amount, address indexed sender, address indexed recipient);

    modifier onlyOwners(){
        bool isOwner = false;
        for(uint i = 0; i<addressOwners.length; i++){
            if(addressOwners[i] == msg.sender){
                isOwner = true;
            }
        }
        require(isOwner == true);
        _;
    }

    constructor(address[] memory _owners, uint _limit){
        addressOwners = _owners;
        numberApprovers = _limit;
    }

    function getOwners() public view returns(address[] memory){
        return addressOwners;
    }

    function deposit() public payable returns(uint){
        myBalance[msg.sender] += msg.value;
        emit depositDone(msg.value, msg.sender);
        return myBalance[msg.sender];
    }
    
    function getBalance() public view returns (uint){
        return myBalance[msg.sender];
    }

    function totalBalance() public view returns(uint) {
        return (address(this)).balance;
    }

    function createTransfer(address payable _receiver, uint _amount) public onlyOwners {
        transferRequests.push(Transfer(transferRequests.length, _receiver, _amount, 0, false));
        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);

    }

    function approveTransfer(uint _index) public onlyOwners {
        require(approvalOfTransfers[msg.sender][_index] == false);
        require(transferRequests[_index].transferSent == false);

        approvalOfTransfers[msg.sender][_index] = true;
        transferRequests[_index].numberOfApprovals++;

        emit ApprovalReceived(_index, transferRequests[_index].numberOfApprovals, msg.sender);

        if (transferRequests[_index].numberOfApprovals >= numberApprovers){
            transferRequests[_index].receiver.transfer(transferRequests[_index].amount);
            transferRequests[_index].transferSent = true;
            emit TransferApproved(_index);
        }
    }

    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    } 
}

Hello there. I’ve tried to dig deep and code all the solution by myself. I’m pretty sure I will find that there were way easier solutions, and I look forward to check how Filip addressed the job!

Considerations:
I’ve really struggled against how mappings and arrays expecially are handled. Not being able to iterate the mapping and having to keep an array of keys (which is gas consuming if has actually to be synced via .pop() ) is really though, but maybe I just have to get used to it.
To mess with arrays I felt like I needed some kind of library to speed up developing, and I created 2 additional contracts for this job.
I feel like for the sake of readability I’ve pushed too far, and there is really a lot of code which make me think if this could really be a realistic approach, since calling a lot of private functions has a gas impact.

Solution
My implementation of a X of Y MultiSig distinguish between an owner and Y signers.

  • Permissions
    • Owner
      • Set X and Y (once, when deploying) [they’re uint8 btw]
      • Add a signer
      • Remove a signer
      • Remove a proposal
      • Remove a signer approvals
      • Remove all approvals
      • Destroy the wallet and rug pull
    • Signer
      • Add a proposal
      • Approve a proposal
      • Revoke his approval
    • Public
      • Check the approvals for a proposal
      • Check the signers
      • Deposit
  • Details
    • For tracking signers
      • Signers addresses are saved in an array
      • Each time a signer is removed the length of the array diminish (I prefer to pop() instead of delete + filter off empty addresses
      • The orders of the signers may change due to how _rmFromAddressArray() works.
    • For tracking proposals
      • All proposals consist basically of a recipient and an amount. That struct Transferral is held by the smart contract inside a mapping called “proposals” which maps the struct id with the actual instance of the struct. I generate the id hashing the struct instance members packed togheter (cfr. _transferralId()). In this way when I have recipient and amount i can get their id and retrieve back the actual storage instance. Since for each bytes32 (proposals key) I would automatically get an empty instance of a Transferral, I consider legit just those instances where key == keccak256(value) check _getTransferralSafe() which implement that logic.
      • Signers approvals are saved inside a mapping called “approvals” which maps their address to an array of bytes32, aka the transferrals id.
      • When a signer is removed his approvals are popped until length == 0 to avoid keeping his approvals in case he would be re-added as a signer after some time.
      • When a proposal is either removed by the owner or after being executed all the signers approvals are whiped out, to avoid a replay attack.
  • Code
    • ./abc/AddressArrayAux.sol
pragma solidity 0.7.5;

contract AddressArrayAux {
    function _rmFromAddressArray(uint idx, address[] storage arrPtr) internal
      returns(address)
      /*
        remove the element at index replacing the indexed element with the last element
        order of the array is NOT preserved
      */
    {
      require(idx < arrPtr.length, "IndexError");
      address ret = arrPtr[idx];
      arrPtr[idx] = arrPtr[arrPtr.length - 1];
      arrPtr.pop();
      return ret;
    }

    function _popFromAddressArray(uint idx, address[] storage arrPtr) internal
      returns(address)
      /*
        remove the element at index and reassign each element just like in python arr.pop(idx)
        order of the array IS preserved
      */
    {
      require(idx < arrPtr.length, "IndexError");
      address ret = arrPtr[idx];
      for (uint i = idx; i < arrPtr.length - 1; i++){
        arrPtr[i] = arrPtr[i + 1];
      }
      arrPtr.pop();
      return ret;
    }

    function _findInAddressArray(address data, address[] storage arrPtr) internal view
      returns(bool, uint)
      /*
        find the first index for which data == arrPtr[index]
        the first returned element is meant to be "found"
        in case of not found the index will be the last index (arrPtr.length-1)

        call like:
        (bool found, uint index) = _findIn<T>Array(data, stateMember);
      */
    {
      bool found = false;
      uint index;
      for (index; index < arrPtr.length; index++){
        if(arrPtr[index] == data){
          found = true;
          break;
        }
      }
      return (found, index);
    }
}
  • ./abc/Bytes32ArrayAux.sol
pragma solidity 0.7.5;

contract Bytes32ArrayAux {

    function _rmFromBytes32Array(uint idx, bytes32[] storage arrPtr) internal
      returns(bytes32)
      /*
        remove the element at index replacing the indexed element with the last element
        order of the array is NOT preserved
      */
    {
      require(idx < arrPtr.length, "IndexError");
      bytes32 ret = arrPtr[idx];
      arrPtr[idx] = arrPtr[arrPtr.length - 1];
      arrPtr.pop();
      return ret;
    }

    function _popFromBytes32Array(uint idx, bytes32[] storage arrPtr) internal
      returns(bytes32)
      /*
        remove the element at index and reassign each element just like in python arr.pop(idx)
        order of the array IS preserved
      */
    {
      require(idx < arrPtr.length, "IndexError");
      bytes32 ret = arrPtr[idx];
      for (uint i = idx; i < arrPtr.length - 1; i++){
        arrPtr[i] = arrPtr[i + 1];
      }
      arrPtr.pop();
      return ret;
    }

    function _findInBytes32Array(bytes32 data, bytes32[] storage arrPtr) internal view
      returns(bool, uint)
      /*
        find the first index for which data == arrPtr[index]
        the first returned element is meant to be "found"
        in case of not found the index will be the last index (arrPtr.length-1)

        call like:
        (bool found, uint index) = _findIn<T>Array(data, stateMember);
      */
    {
      bool found = false;
      uint index;
      for (index; index < arrPtr.length; index++){
        if(arrPtr[index] == data){
          found = true;
          break;
        }
      }
      return (found, index);
    }
}
  • ./abc/Ownable.sol
pragma solidity 0.7.5;

contract Ownable {
  address internal owner;
  
  constructor(){
    owner = msg.sender;
  }

  modifier onlyOwner {
    require(msg.sender == owner, "Owner only");
    _;
  }
}
  • ./abc/DestroyableByOwner.sol
pragma solidity 0.7.5;

import "./Ownable.sol";

contract DestroyableByOwner is Ownable {
  function destroy() public onlyOwner {
    selfdestruct(payable(owner));
  }
}
  • ./XOfYMultiSig.sol
pragma solidity 0.7.5;
pragma abicoder v2;

import "./abc/Ownable.sol";
import "./abc/DestroyableByOwner.sol";
import "./abc/Bytes32ArrayAux.sol";
import "./abc/AddressArrayAux.sol";

contract XOfYMultiSig is 
  Ownable, 
  DestroyableByOwner, 
  Bytes32ArrayAux, 
  AddressArrayAux {
  /*
    Implementation of a X of Y MultiSig
  */
  address[] public signers;
  uint8 X;
  uint8 Y;
  mapping(address => bytes32[]) approvals;
  mapping(bytes32 => Transferral) proposals;

  struct Transferral {
    address recipient;
    uint amount;
  }

  constructor (uint8 _X, uint8 _Y) Ownable() {    
    // super() in function header
    require(_X > 0 && _Y > _X, "Wrong parameters");
    X = _X;
    Y = _Y;
  }

  /***
  *  Signers mgmt
  */
  modifier signersNotFull {
    require(signers.length < Y, "Too many signers");
    _;
  }
  modifier signersNotEmpty {
    require(signers.length > 0, "No signer");
    _;
  }
  modifier callerIsSigner {
    require(_isSigner(msg.sender), "Signers only");
    _;
  }
  modifier signersOrOwnerOnly {
    require(
      msg.sender == owner || _isSigner(msg.sender), 
      "Signers or Owner only"
    );
    _;
  }

  event signerAdded(address indexed signer);
  event signerRemoved(address indexed signer);
  event signerApprovalsCleared(address indexed signer);

  // isSigner
  function _isSigner(address addr) private view
    returns(bool){
    (bool ret, uint idx) = _findInAddressArray(addr, signers);
    return ret;
  }

  // rmSigner
  function _rmSigner(uint idx) private {
    address signer = signers[idx];
    uint preSignersLen = signers.length;
    
    // if it's ok to mess with array order:
    _rmFromAddressArray(idx, signers);

    // if array order matters (more gas consumed)
    // _popFromAddressArray(idx, signers);

    assert(signers.length == preSignersLen - 1);
    assert(!_isSigner(signer));
  }
  function _rmSigner(address signer) private {
    (bool isSigner, uint signerIdx) = _findInAddressArray(signer, signers);
    require(isSigner, "Not a signer");
    _rmSigner(signerIdx);
  }

  // hasApproved
  function _hasApproved(address signer, bytes32 tfid) private view
    returns(bool){
    for (uint i = 0; i < approvals[signer].length; i++){
      if (approvals[signer][i] == tfid){
        return true;
      }
    }
    return false;
  }
 
  // clearSignerApprovals
  function _clearSignerApprovals(address signer) private {
    bytes32[] storage _approvals = approvals[signer];
    while(_approvals.length > 0){
      _approvals.pop();
    }
  }

  //
  // Public methods
  //
  function addSigner(address signer) public onlyOwner signersNotFull {
    require(!_isSigner(signer), "Already a signer");
    signers.push(signer);
    emit signerAdded(signer);
    assert(signers.length <= Y);
  }

  function rmSigner(address signer) public onlyOwner signersNotEmpty {
    (bool isSigner, uint signerIdx) = _findInAddressArray(signer, signers);
    require(isSigner, "Not a signer");
    
    _rmSigner(signerIdx);
    emit signerRemoved(signer);

    // gas consuming, but needed to avoid retrieving back old approvals 
    // if the signer is re-added
    _clearSignerApprovals(signer);
    emit signerApprovalsCleared(signer);
  }


  /***
  *  Transferrals mgmt
  */
  event transferralProposed(
    address indexed proposer,
    address indexed recipient,
    uint amount
  );
  event transferralApproved(
    address indexed signer,
    address indexed recipient,
    uint amount
  );
  event approvalRevoked(
    address indexed signer,
    address indexed recipient,
    uint amount
  );
  event quorumReached(
    address indexed recipient,
    uint amount
  );
  event proposalRemoved(
    address indexed recipient,
    uint amount
  );
  
  // getTransferralSafe
  // has require checks
  function _getTransferralSafe(bytes32 tfid) private view
    returns(Transferral storage, bytes32){
    require(_transferralInProposals(tfid), "Transferral not yet proposed");
    Transferral storage transferral = proposals[tfid];
    require(_transferralIsValid(transferral), "Transferral not valid");
    return (transferral, tfid);
  }
  function _getTransferralSafe(address recipient, uint amount) private view
    returns(Transferral storage, bytes32){
    return _getTransferralSafe(_transferralId(Transferral(recipient, amount)));
  }

  // transferralId
  function _transferralId(address recipient, uint amount) private pure
    returns(bytes32){
    return keccak256(abi.encodePacked(recipient, amount));
  }
  function _transferralId(Transferral memory transferral) private pure
    returns(bytes32){
    return _transferralId(transferral.recipient, transferral.amount);
  }

  // transferralIsValid
  function _transferralIsValid(Transferral memory transferral) private pure
    returns(bool){
    return transferral.amount > 0;
  }

  // transferralIsBacked
  function _transferralIsBacked(Transferral memory transferral) private view
    returns(bool){
    return transferral.amount < address(this).balance;
  }

  // transferralInProposals
  function _transferralInProposals(bytes32 tfid) private view
    returns(bool){
    return _transferralId(proposals[tfid]) == tfid;
  }
  function _transferralInProposals(Transferral memory transferral) private view
    returns(bool){
    return _transferralInProposals(_transferralId(transferral));
  }

  // transferralHasQuorum
  function _transferralHasQuorum(bytes32 tfid) private view
    returns(bool){
    uint8 _approvals;
    for (uint i = 0; i < signers.length; i++){
      if (_hasApproved(signers[i], tfid)){
        _approvals++;
      }
    }
    return _approvals >= X;
  }
  function _transferralHasQuorum(Transferral memory transferral) private view
    returns(bool){
    return _transferralHasQuorum(_transferralId(transferral));
  }

  // approveTransferral
  function _approveTransferral(bytes32 tfid) private callerIsSigner {
    approvals[msg.sender].push(tfid);
    assert(_hasApproved(msg.sender, tfid));
  }
  function _approveTransferral(Transferral memory transferral) private {
    return _approveTransferral(_transferralId(transferral));
  }
  
  // revokeApproval
  function _revokeApproval(bytes32 tfid) private callerIsSigner {
    (bool hasApproved, uint approvalIdx) = _findInBytes32Array(tfid, approvals[msg.sender]);
    require(hasApproved, "Signer hasn't approved this Transferral");

    // if it's ok to mess with array order:
    _rmFromBytes32Array(approvalIdx, approvals[msg.sender]);

    // if array order matters (more gas consumed)
    // _popFromBytes32Array(approvalIdx, approvals[msg.sender]);
    
    assert(!_hasApproved(msg.sender, tfid));
  }
  function _revokeApproval(Transferral storage transferral) private {
    return _revokeApproval(_transferralId(transferral));
  }

  // clearTransferralApprovals
  function _clearTransferralApprovals(bytes32 tfid) private {
    for (uint i=0; i < signers.length; i++){
      (bool hasApproved, uint approvalIdx) = _findInBytes32Array(tfid, approvals[signers[i]]);
      if (hasApproved){
        _rmFromBytes32Array(approvalIdx, approvals[signers[i]]);
      }
    }
    assert (!_hasApprovals(tfid));
  }

  // rmProposal
  function _rmProposal(bytes32 tfid) private {
    delete proposals[tfid];
    assert (!_transferralInProposals(tfid));
  }

  // getApprovals
  function _getApprovals(bytes32 tfid) private view
    returns(uint8){
    uint8 ret;
    for(uint i=0; i<signers.length; i++){
      if(_hasApproved(signers[i], tfid)){
        ret++;
      }
    }
    return ret;
  }

  // hasApprovals
  function _hasApprovals(bytes32 tfid) private view
    returns(bool){
    for(uint i=0; i<signers.length; i++){
      if(_hasApproved(signers[i], tfid)){
        return true;
      }
    }
    return false;
  }

  //
  // Public methods
  //
  function approveTransferral(address recipient, uint amount) public callerIsSigner {
    (Transferral storage transferral, bytes32 tfid) = _getTransferralSafe(recipient, amount);
    require(_transferralIsBacked(transferral), "Wallet Balance doesn't cover Transferral amount");
    require(!_hasApproved(msg.sender, tfid), "Signer has already approved this Transferral");
    
    _approveTransferral(tfid);
    emit transferralApproved(msg.sender, recipient, amount);

    // check if quorum is reached
    if (_transferralHasQuorum(tfid)){
      emit quorumReached(recipient, amount);
      // execute the transfer
      bool sent = _transfer(payable(recipient), amount);
      require(sent, "Transfer failed");
      emit trasferExecuted(recipient, amount);
      
      // revokeApprovals to avoid repay attacks
      _clearTransferralApprovals(tfid);
      _rmProposal(tfid);
      // not emitting a signal here
      // assume that a trasferExecuted signal also signal that
      // current signers approvals of the same Transferral
      // has been cleared
    }
  }

  function revokeApproval(address recipient, uint amount) public callerIsSigner {
    (Transferral storage transferral, bytes32 tfid) = _getTransferralSafe(recipient, amount);
    _revokeApproval(tfid);
    emit approvalRevoked(msg.sender, recipient, amount);
  }

  function proposeTransferral(address recipient, uint amount) public callerIsSigner {
    Transferral memory tf = Transferral(recipient, amount);
    bytes32 tfid = _transferralId(tf);
    require(!_transferralInProposals(tfid), "Transferral already proposed");
    require(_transferralIsValid(tf), "Transferral not valid");
    require(_transferralIsBacked(tf), "Wallet Balance doesn't cover Transferral amount");
    
    proposals[tfid] = tf;
    emit transferralProposed(msg.sender, recipient, amount);

    // auto approve when proposing
    _approveTransferral(tfid);
    emit transferralApproved(msg.sender, recipient, amount);
  }

  function getApprovals(address recipient, uint amount) public view 
    returns(uint8){
    (Transferral storage transferral, bytes32 tfid) = _getTransferralSafe(recipient, amount);
    return _getApprovals(tfid);
  }

  /***
  *  Owner additional mgmt
  */
  function clearAllApprovals() public onlyOwner signersNotEmpty {
    for (uint i=0; i < signers.length; i++){
      _clearSignerApprovals(signers[i]);
      emit signerApprovalsCleared(signers[i]);
    }
  }

  function clearSignerApprovals(address signer) public onlyOwner signersNotEmpty {
    require(_isSigner(signer), "Not a signer");
    _clearSignerApprovals(signer);
    emit signerApprovalsCleared(signer);
  }

  function rmProposal(address recipient, uint amount) public onlyOwner {
    (Transferral storage transferral, bytes32 tfid) = _getTransferralSafe(recipient, amount);
    _clearTransferralApprovals(tfid);
    _rmProposal(tfid);
    emit proposalRemoved(recipient, amount);
  }


  /***
  *  Ether IO
  */
  event depositCredited(
    address indexed donor,
    uint amount
  );
  event trasferExecuted(
    address indexed recipient,
    uint amount
  );
  function deposit() public payable {
    emit depositCredited(msg.sender, msg.value);
  }
  function _transfer(address payable to, uint amount) private signersOrOwnerOnly 
    returns(bool){
    (bool sent, bytes memory data) = to.call{value: amount}("");
    return(sent);
  }
   
}

Final thoughts and questions
As I said I’m looking forward to check the actual solution and look for a shorter implementation. When coding the aux contracts for handling arrays I really was asking myself if there wasn’t some kind of templating (like cpp) to avoid to basically duplicate all the code just for sake of the array type (would be awesome to write some template function like _findInArray(<T> needle, <T>[] haystack) returns(uint) and use it with multiple types.
I will really appreciate your feedback and your advice about this implementation.
This course was really good, I literally just ate it in 48h and I’m just getting hungrier :joy:

Here is the solution that I found

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
pragma abicoder v2;

contract MultiSigWallet{

    event Deposited(address from, uint amount);
    event RequestAdded(address to, uint amount, address from);
    event RequestConfirmed(address from);
    event TransactionSent(address to, uint amount);

    address[] Owners;
    uint numConfirmation;

    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool confirmed;
        address initiater;
    }
    Transfer[] TransferRequests;

    mapping(address => mapping(uint => bool)) confirmations;
    mapping(address => bool) IsOwner;
    mapping(address => uint) Balances;

    modifier onlyOwners(){
        bool owner = false;
        for(uint i=0; i < Owners.length; i++){
            if(Owners[i] == msg.sender){
                owner = true;
                break;
            }
        }
        require(owner == true);        
        _;
    }

    constructor(address[] memory _owners, uint _limit) {
        for(uint i=0; i < Owners.length; i++){
            IsOwner[_owners[i]] = true;

        }

        Owners = _owners;
        numConfirmation = _limit;
    }

    function deposit() public payable returns(uint){
        require(Balances[msg.sender] >= 0);
        Balances[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
        return(Balances[msg.sender]);
    }

    function getBalance() public view returns(uint){
        return(Balances[msg.sender]);
    }

    function createTransfer(address payable receiver, uint amount) public onlyOwners{
        require(msg.sender != receiver);
        require(Balances[msg.sender] >= amount);

        Transfer memory newTransfer = Transfer(amount, receiver, 0, false, msg.sender);
        emit RequestAdded(receiver, amount, msg.sender);
        TransferRequests.push(newTransfer);
    }

    function getTransferRequests(uint _id) public view returns (Transfer[] memory){
        _id = TransferRequests.length;
        return(TransferRequests);
    }

    function ApproveTransaction (uint _id) public onlyOwners {

        require(TransferRequests[_id].confirmed == false);
        require(TransferRequests[_id].initiater != msg.sender);
        require(confirmations[msg.sender][_id] == false);       
        TransferRequests[_id].approvals ++;
        emit RequestConfirmed(msg.sender);

        if(TransferRequests[_id].approvals >= numConfirmation){
            TransferRequests[_id].confirmed = true;
            Balances[msg.sender] -= TransferRequests[_id].amount;
            emit TransactionSent(TransferRequests[_id].receiver, TransferRequests[_id].amount);
            TransferRequests[_id].receiver.transfer(TransferRequests[_id].amount);
        }
    }

    function getApprovals(uint _id) public view returns(uint){
        return(TransferRequests[_id].approvals);
        
    }

    function getOwners() public view returns(address[] memory){
        return(Owners);
    }                        

}
1 Like

Hi Everyone,
This is my first crack at the multisig wallet not having looked at any of the videos. The issues I’m running into are spelled out in the comments. Would appreciate any help. Thanks!

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.7.5;
pragma abicoder v2;

contract MultisigWallet {

/* Sets the number of owners and the number of owners needed to approve a transaction. These values are stored in
variables to make the contract easier to customize*/
    uint numberOfOwners = 3;
    uint ownersToApprove = 2;

/* Maps every address to a boolean that should start set to false for all contracts and will be set to true when an
address is set as an owner*/
    mapping (address => bool) Ownership;

/* Events to be emitted when something happens*/
    event NewDeposit(address from, uint amount);
    event NewRequest(address to, uint amount);
    event TransferInitiated(address to, uint amount);

/* Creates a dynamic array of the owners. It may be more elegant to set the size of the array to numberOfOwners, but then
I wouldn't be able to use .push to add the owners so it's not perfect either way. Would love to know if there's a better 
way. The size of the array is limited by a "require" statement in the setOwner function*/
    address[] Owners;

/* The constructor adds the address deploying the contract to the 0 index of the Owners array and changes its Ownership
boolean to "true" */
    constructor() {
        
        Owners.push(msg.sender);
        Ownership[msg.sender] = true;

    }

/* A function modifier that only allows the function to be called if the address calling it has had its Ownership boolean
set to true by either the constructor or by setOwner. For some reason, this does not seem to be working, as calling setOwner
from an address that hasn't had it's ownership boolean changed to "true" still works and lets any address set an owner. */
    modifier onlyOwner {
        require(Ownership[msg.sender] = true);
        _;
    }

/* This function works as intended except it is allowing anyone to call the function regardless of the onlyOwner modifier
that should be preventing the contract from being called by an address that has not been set as an owner. */
    function setOwner(address _newOwner) public onlyOwner {
        require(Owners.length < numberOfOwners, "Maximum number of owners reached.");
        Owners.push(_newOwner);
        Ownership[_newOwner] = true;
    }

/* This function works as intended. */
    function getOwner(uint _ownerId) public view returns (address) {
        return Owners[_ownerId];
    }

/* This function works initially, but after calling deposit, this still returns 0. I think it is a problem with the deposit 
function, but the error could be here as well. */
    function getBalance() public view returns (uint) {
        address payable walletAddress = payable(address(this));
        return walletAddress.balance;
    }   

/* This function throws an error that says "Note: The called function should be payable if you send value and the value 
you send should be less than your current balance." This happens even when I deposit a value of 1 and have over 100 eth in the
address I'm calling the function from. If I change the last line to read "walletAddress.send(_amount);" It doesn't throw
that error, but the eth doesn't transfer to the new wallet. */
    function deposit(uint _amount) public payable {
        address payable walletAddress = payable(address(this));
        walletAddress.transfer(_amount);
    }

/* I haven't began testing out the functions past this point, so no need to comment on them yet. Thanks! */
    struct TxRequest {
        address requester;
        address to;
        uint amount;
        uint approvals;
    }

    TxRequest[] requests;

    function createTxRequest(address _to, uint _amount) public onlyOwner returns (uint) {
        requests.push(TxRequest(msg.sender, _to, _amount, 1));
        uint _requestId = requests.length - 1;
        return _requestId;
    }

    function approveTxRequest(uint _txId) public onlyOwner {
        require(msg.sender != requests[_txId].requester, "You cannot approve your own transaction");
        requests[_txId].approvals++;
        if (requests[_txId].approvals == ownersToApprove) {
            initiateTransfer(_txId);
        }
    }

    function initiateTransfer(uint _txId) internal {
        address payable _to = payable(requests[_txId].to);
        uint _amount = requests[_txId].amount;
        _to.transfer(_amount);
    }

    }

This certainly could use some improvements after having looked at some others’. However this should work as far as I understand

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.7.5;

import “./MultiOwner.sol”;

import “./Destroyable.sol”;

contract MultiSigWallet {

uint256 private balance = 0;

TransferToBeSigned [] transferRequests;

mapping(address => mapping(uint => bool)) signed;

mapping (address => bool) owners;

address [] ownerList;

uint signees;

struct TransferToBeSigned{

    uint amount;

    address payable recipient;

    bool executed;

}

modifier onlyOwners {

    require(owners[msg.sender] == true);

    _; //run the function

}

constructor(address [] memory o, uint _s){

    signees = _s;

    ownerList = o;

    for(uint i = 0; i < o.length; i++){

        owners[o[i]] = true;

    }

}

function deposit() public payable returns (uint)  {

    balance += msg.value;

    return balance;

}



function getBalance() public view returns (uint){

    return balance;

}

function transferRequest(address recipient, uint amount) public onlyOwners {

    TransferToBeSigned memory tr = TransferToBeSigned(amount, recipient, false);

    transferRequests.push(tr);

}



function sign(uint transaction_id) public onlyOwners{

    require(signed[msg.sender][transaction_id] != true)

    signed[msg.sender][transaction_id] = true;

}

function startTransfer(uint transaction_id) public onlyOwners{

    uint approvals = 0;

    for(uint o = 0; o < ownerList.length; o++){

        //address a = ownerList[o];

        if(signed[ownerList[o]][transaction_id]){

            approvals++;

        }

    }

    if(approvals >= signees && !(transferRequests[transaction_id].executed)){

        transfer(transferRequests[transaction_id].recipient, transferRequests[transaction_id].amount);

    }

}

function transfer(address recipient, uint amount) private {

    require(balance >= amount, "Balance not sufficient");

    require(msg.sender != recipient, "Don't transfer money to yourself");

   

    uint previousSenderBalance = balance;

    balance -= amount;

    assert(balance == previousSenderBalance - amount);

}

}

1 Like

Hi,

Is a work in progress my attempt at multisig wallet

Can anybody tell me why the complier tells me I have a undecleared identifier in the code where I try to push a Transfer object into my trasferrequests array?

pragma solidity 0.8.11;


contract Multisig {

    struct externalTransfer {
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }

    mapping(address => mapping(uint => bool)) approvals;

    event depositDone(uint amount, address depositedFrom);

    address payable Owner;
    address payable CoOwnerA;
    address payable CoOwnerB;

    externalTransfer[] transferRequests ;

    modifier onlyOwners(){
        require(msg.sender == Owner || msg.sender == CoOwnerA || msg.sender == CoOwnerB);
        _;
    }

    constructor () {
        Owner = payable(msg.sender);
        CoOwnerA = payable(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        CoOwnerB = payable(0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db);
        uint limit = 3; 
    }

    //Function that recives ETH into wallet
    function Deposit () public payable {
        
        emit depositDone(msg.value, msg.sender);     
    }

    //function to transfer ETH out of wallet to external adress
    function createExternalTransfer (uint _amount, address _recipient) public onlyOwners {
       require(address(this).balance >= _amount, "Insufficient funds");
       transferRequests.push(externalTransfer(_amount, _receiver, 0, false, transferRequests.length));
    }

    function Approve (uint _id) public onlyOwners {
        
    }

    function getWalletBalance () public view returns (uint) {
        return address(this).balance;
    }

    function getTransferRequests () public view returns (uint) {
        return transferRequests[];
    }

}

Hi @Tomaage,

It’s because you are referencing _receiver (which is the name of the property in the struct with an underscore added) and not _recipient (which is the name of the parameter you want to assign to receiver) :wink:

Once you correct that, then you’ll get another error, because the receiver property in the struct is a payable address, and so the value you assign to it must also be payable.

After correcting that line of code, you’ll get another error for the return statement in this function …

You’re missing a reference to an index number between the square brackets, unless you want to return the whole array, in which case you only need to reference the name of the array:

return transferRequests;

Then you need to change the data type you are returning …

returns (uint)

… so that it corresponds with the actual value being returned:

  • an externalTransfer struct instance (object); or
  • an array of externalTransfer struct instances (objects).

You’ll then have another issue to resolve after that (relating to the final line in your constructor), but I’ll let you work that one out on your own :muscle: