Project - Multisig Wallet

Hello,

This is what I came up with for the multisig wallet, before looking at the assistance videos. It’s a bit different from Filips solution but seemed to work in my remix testing. I used a mapping inside the struct instead of double mapping. I also used a mapping for owner addresses instead of an array.

Any feedback or improvement ideas are appriciated. :blush:

Code:

pragma solidity 0.8.1;

contract MultisigWallet {
 
    mapping (address => bool) ownerAddresses;
    uint approvalsNeeded;

 constructor(address[] memory _ownerAddresses, uint _approvalsNeeded) {
     uint i = 0;
     while (i < _ownerAddresses.length) {
         ownerAddresses[_ownerAddresses[i]] = true;
         i++;
     }
     approvalsNeeded = _approvalsNeeded;
 }
 
 struct transferRequest {
     uint amount;
     address recipientAddress;
     uint approvals;
     mapping (address => bool) ownerApprovals;
 }
 
 transferRequest[] tRarray;
    
    function deposit() public payable {
        
    }
    
    function requestTransfer(uint _amount, address _address) public returns (uint){
        require(ownerAddresses[msg.sender] == true, "Not owner");
        uint id = tRarray.length;
        tRarray.push();
        transferRequest storage tR = tRarray[id];
        tR.amount = _amount;
        tR.recipientAddress = _address;
        return id;
    }
    
    function approve(uint _id) public {
        require(ownerAddresses[msg.sender] == true, "Not owner");
        require(tRarray[_id].ownerApprovals[msg.sender] != true, "Already approved");
        tRarray[_id].ownerApprovals[msg.sender] = true;
        tRarray[_id].approvals++;
        
        if (tRarray[_id].approvals >= approvalsNeeded) {
            payable(address(tRarray[_id].recipientAddress)).transfer(tRarray[_id].amount);
            delete tRarray[_id];
        }
    }
}

Thanks!

1 Like

hey @SergeyG, very good man youve really worked hard on this and tried a lot of different solutions its great to see. Yeah i see what you mean bout the reusing of ID’s but even still the gas consumption is a good it less from your first solution its crazy how may different ways tou can approach the same problem sometimes isnt it . But your work has deffo paid off enjoy the next course whatever you take and im sure ill see you around in the various forums.

Evan

1 Like
pragma solidity 0.7.5;
pragma abicoder v2;

/// @title A multi-signature wallet that requires the approval of a certain number of owners to execute transactions.
/// @author Eerina Haque 
/// @notice This wallet allows only for deposits and transfers. There is no interaction with external contracts or applications.
contract Wallet {
    address[] public owners;
    uint public sigsRequired;
    mapping(address => bool) public isOwner; 
    mapping(uint => mapping(address => bool)) isSigned;
    
    struct Transaction {
        uint transactionIndex;
        address payable to; 
        address from;
        uint value; 
        bool executed; 
        uint numSigs;
    }
    
    Transaction[] public transactions;
    
    constructor(address[] memory _owners, uint _sigsRequired) {
        require(_owners.length > 0, "owners required");
        require(_sigsRequired > 0 && _sigsRequired <= _owners.length,
            "invalid number of required signatures");
        sigsRequired = _sigsRequired;
        
        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "invalid address");
            require(isOwner[owner] != true, "not a unique owner");
            
            isOwner[owner] = true;
            owners.push(owner);
        }
    }
    
    /* Events */
    /// @notice Logs the value of funda deposited into the contract. It can be queried by the address of the account that deposited.
    /// @param _account The account that deposited funds.
    /// @param _value The value of the funds that were transacted.
    event Deposited(address indexed _account, uint _value);
    
    /// @notice Logs the index of an approved transaction. It can be queried by the address of the account that approved it.
    /// @param _account The account that approved the transaction.
    /// @param _transactionIndex The index of the transaction that was approved.
    event TransactionApproved(address indexed _account, uint _approvals, uint _transactionIndex);
    
    /// @notice Logs the index of an executed transaction. It can be queried by the address of the account that executed it.
    /// @param _account The account that executed the transaction.
    /// @param _transactionIndex The index of the transaction that was executed.
    event TransactionExecuted(address indexed _account, uint _transactionIndex);
    
    /* modifiers */
    modifier onlyOwner {
        require(isOwner[msg.sender]);
        _;
    }
 
    /* functions */ 
    /// @notice Allows any user to deposit funds into the contract.
    function deposit() public payable {
        emit Deposited(msg.sender, msg.value);
    }
    
    /// @notice Allows an owner to create a request for transferring funds out of the wallet.
    /// @param _to The address that the funds are being sent to.
    /// @param _value The value of the funds that are being transferred.
    function requestTransaction(address payable _to, uint _value) public onlyOwner {
        require(address(this).balance >= _value, "not enough funds in the contract to request transaction");
        
        transactions.push(Transaction({
            transactionIndex: transactions.length,
            to: _to,
            from: msg.sender,
            value: _value,
            executed: false,
            numSigs: 0
        }));
    }
    
    /// @notice Allows an owner to approve a specified transaction. Automatically executes transaction if the
    /// required number of approvals are met.
    /// @param _transactionIndex The index of the transaction that is to be approved.
    function approveTransaction(uint _transactionIndex) public onlyOwner {
        require(_transactionIndex < transactions.length);   // checks if the transaction exists 
        require(!isSigned[_transactionIndex][msg.sender]);  // checks if the transaction is already approved.
        require(!transactions[_transactionIndex].executed); // checks if the transaction has already been executed.
        
        transactions[_transactionIndex].numSigs++;
        isSigned[_transactionIndex][msg.sender] = true;
        emit TransactionApproved(msg.sender, transactions[_transactionIndex].numSigs, _transactionIndex);
        
        if(transactions[_transactionIndex].numSigs >= sigsRequired) { 
            transactions[_transactionIndex].executed = true;
            transactions[_transactionIndex].to.transfer(transactions[_transactionIndex].value);
        }
    }
    
    /// @notice Gives any user access to all of the transactions that have been requested.
    /// @return All of the transactions that have been requested in this contract.
    function getTransactions() public view returns(Transaction[] memory) {
        return transactions;
    }
    
    function getBalance() public view returns(uint contractBalance) {
        return address(this).balance;
    }
}
1 Like

Hey @jeanphi, hope you are ok.

I have reviewed your contract, is quite nice but there is always room for improvements :nerd_face:

Some suggestions:
With your requestTransfer function, although is working as expected, there is a minor improvement with the last require which will calculate that the smart contract balance minus the totalRequested is less than the amount, the problem is that, when that condition is triggered the transaction will be reverted but the cost of execution for the loop iteration will still be charged.

    function requestTransfer (address _recipient, uint _amount) public onwersAccessOnly returns (uint) {
        require(_amount!=0,"Cannot request a transfer with 0 amount");
        require(address(this).balance>=_amount,"Not enough money in the wallet to accept the transfer request");
        uint totalRequested=0;
        uint size=0;
        //to check if this key exist
        while (transferRequests[size].amount!=0){
            if (!transferRequests[size].paid){
                totalRequested+=transferRequests[size].amount;
            }
            size++;
        }
        require(address(this).balance-totalRequested>=_amount,"Too much money already pledged to accept the transfer request");
        TransferRequest memory _transferRequest=TransferRequest(size,_recipient,new address[](0),_amount,false);
        transferRequests[size]=_transferRequest;
        transferRequests[size].acceptedBy.push(msg.sender);
        
        emit transferRequested(_transferRequest.id,msg.sender,_transferRequest.recipient,_transferRequest.amount);
        _attemptApproval(size);
        return size;
    }

Also i see that many of this functions calls _transfertRequestExists to validate if the transactions exist, it might be better to do something like this:

    function _transfertRequestExists (uint _transfertRequestId) internal view  returns(bool){
        //way of checking if key exists
        return transferRequests[_transfertRequestId].recipient != address(0);
    }

This will return false if the transaction recipient address is a zero address (in a mapping, all non-existence values are settle to 0 by default).

Then you can use it this way on all the other functions

require(_transfertRequestExists(_transfertRequestId), "operator query for nonexistent transaction");

Also your approve function can be triggered multiple times, it does not matter if the msg.sender already triggered before, so you might need a way to verify if the transactions has been already approved by the msg.sender.

Overall you have made a very nice work! :muscle:

Carlos Z

1 Like

After watching the first video, I took a few days to create my multi-sig wallet contract without the use of the additional hint videos as a personal challenge. I broke the functionality into 2 contracts: one to handle the wallet functions, the other to handle the verified users/signers functions. Anybody can deposit funds into the multi-sig wallet, only the verified signers can request and approve a withdrawal/transfer from the multi-sig wallet. Only verified signers can update the verified signer list (in case somebody loses their private key). Current code is meant to be a MVP with room for upgrades to enhance security and allow for greater simplicity. Suggestions and fixes very much welcomed and appreciated! :slight_smile:

> pragma solidity 0.7.5;
> pragma abicoder v2;
> 
> import "./MultiSigOwners.sol";
>     //second contract OwnerList with whitelisted addresses
>         //functions to verify and control signatory addresses
> 
> 
> contract MultiSigWallet is OwnerList {
>     
>     mapping(address => uint) balance;
> 
>     //event to keep track of deposits & transfers; indexed
>     event depositDone(uint amount, address indexed depositedFrom);
>     event transferDone(uint amount, address indexed transferTo);
>     event confirmationDone(address requester, uint amount, address indexed confirmSignature);
>     
>     //could have declared a better name, meant to track transfer requests
>     //Potential Upgrade - add function to auto-remove requests with successful transfers
>     struct pendingRequest{
>         address transferRequester;
>         uint transferAmount;
>         address transferTo;
>         bool transferConfirmed;
>     }
>     
>     pendingRequest[] requests;
>     
>     //function getUserBalance
>     function getUserBalance() public view returns (uint){
>         return balance[msg.sender];
>     }
>     
>     //function getWalletBalance
>     function getWalletBalance() public view returns (uint){
>         return balance[address(this)];
>     }
>     
>     //deposit funds into wallet from user
>     function depositWallet() public payable returns (uint){
>         //check balance of msg.sender
>         require(balance[msg.sender] >= msg.value, "Balance not sufficient");
>          _transfer(msg.sender, address(this), msg.value);
>         
>         //log deposit
>         emit depositDone(msg.value, msg.sender);
>         return balance[msg.sender]; 
>     }
>     
>     //deposit funds into user's wallet for testing
>     function depositUser() public payable returns (uint){
>         balance[msg.sender] += msg.value;
>         emit depositDone(msg.value, msg.sender);
>         return balance[msg.sender]; 
>     }
>     
>     //create a transfer Request by verified user to be confirmed by 2nd verified signer
>     function transferRequest(address recipient, uint amount) public {
>         require(address(this) != recipient, "Do not send funds to yourself");
>         
>         //verify signatory address 
>         require(verifySignPerson(msg.sender) == true, "Failed signatory verification");
>         
>         bool _transferConfirmed = false;
>         requests.push(pendingRequest(msg.sender, amount, recipient, _transferConfirmed));
>     }
>     
>     //function getRequest
>     function getRequest(uint _index) public view returns(pendingRequest memory){
>         return requests[_index];
>     }
>     
>     
>     //confirm a selected Request by 2nd verified signer
>     function confirmRequest(uint _index) public returns(uint){
>         require(verifySignPerson(msg.sender) == true, "Failed signatory verification");
>         require(msg.sender != requests[_index].transferRequester, "Requires two unique authorized signers");
>         require(requests[_index].transferConfirmed == false, "Cannot reconfirm a request");
>         require(signers.length == 3, "Must have 3 verified addresses");
>         require(_index < requests.length, "Specified request does not exist");
>         
>         uint previousSenderBalance = balance[address(this)];
>         require(previousSenderBalance - requests[_index].transferAmount >= 0,"Cannot transfer more than available amount");
>         
>         //commits the transfer based on data in requests
>         requests[_index].transferConfirmed = true;
>         _transfer(address(this), requests[_index].transferTo, requests[_index].transferAmount);
>         
>         assert(balance[address(this)] == previousSenderBalance - requests[_index].transferAmount);
>         emit transferDone(requests[_index].transferAmount, requests[_index].transferTo);
>         emit confirmationDone(requests[_index].transferRequester, requests[_index].transferAmount, msg.sender);
>         return balance[address(this)];
>     }
>     
>     
>     //function _transfer
>     function _transfer(address from, address to, uint amount) private {
>         balance[from] -= amount;
>         balance[to] += amount;
>     }
> 
> }

Next contract

pragma solidity 0.7.5;
pragma abicoder v2;


contract OwnerList{
    
    //structure with whitelisted signatory addresses
    struct SignPerson {
        string name;
        address signAddress;
    }
    
    SignPerson[] signers;
    
    //function to add signatory address for a max of 3 signers
    function addSignPerson(string memory _name) external{
        //verify less than 3 signers exist
        require(signers.length <= 2, "Only 3 signing addresses allowed");
        
        //verify new address is unique to existing rows
        if(signers.length > 0){
            for (uint i = 0; i <= signers.length-1; i++){
                require(signers[i].signAddress != msg.sender, "Cannot be the same as existing address");
            }
        }
        
        signers.push( SignPerson(_name, msg.sender) );
        
        //verify less than 4 signers exist
        assert(signers.length <= 3);
    }
    
    //function to delete signatory address
    //Potential Upgrade - have delete require 2 verified signatures for security purposes
    function deleteSignPerson(uint _index) external returns (uint) {
        //verify 3 signers currently exist
        require(signers.length == 3, "Cannot remove signer unless 3 total signers exist");
        require(_index < signers.length, "Selection must be in structure range");
        
        //verify signatory address
        require(verifySignPerson(msg.sender) == true, "Current address does not qualify");
        
        //remove address from structure
        for (uint i = _index; i < signers.length-1; i++){
            signers[i] = signers[i+1];
        }

        signers.pop();
        return signers.length;
    }
    
    //function to get signatory address structure
    function getSigner(uint _index) external view returns (SignPerson memory, uint){
        return (signers[_index], signers.length);
    }
    
    //function to verify current user is a valid signer
    function verifySignPerson(address _userAddress) internal view returns (bool){
        bool verifiedOne = false;
        
        if(signers.length > 0){
            for (uint i = 0; i <= signers.length-1; i++){
            if(signers[i].signAddress == _userAddress){
                verifiedOne = true;
                }
            }
        }
        return verifiedOne;
    }
    //Potential Upgrade - Have a whitelist of transfer/withdrawal addresses
}
1 Like

A couple of questions:

  1. My emits don’t seem to be displaying the details specified in the events, when I run the related functionality. Have I missed something? What should be displayed?

  2. I tried to add functionality to restrict duplicate owner addresses in the contract deployment, but couldn’t get it to work. I added it as a modifier on the constructor, which may have been the problem. However, is this functionality even needed? Philip made mention of it in the final video, but wouldn’t the other functions cause errors anyway (i.e. - owner can’t approve same request twice)?

Thank you in advance for any assistance!

pragma solidity 0.7.5;
pragma abicoder v2;

    //MULTI-SIG WALLET CONTRACT
contract Wallet {

        //CREATE ARRAY OF WALLET OWNERS AND NUMBER OF REQUIRED APPROVALS
    address[] public owners;
    uint minimumApprovals;

        //CREATE DATA STRUCTURE FOR TRANSFER REQUEST INSTANCES
    struct Transfer{
        uint amount;
        address payable recipient;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }

        //CREATE TRANSFER REQUEST STRUCTURE
    Transfer[] transferRequests;
    
        //CREATE EVENT LOGS
    event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _receiver);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);

        //DOUBLE MAPPING: OWNER TO REQUEST ID TO APPROVED-OR-NOT
    mapping(address => mapping(uint => bool)) approvals;
     
        //connect unique address to balance     
    mapping(address => uint) balance;

        //ENSURE ONLY OWNERS CAN EXECUTE A TRANSFER or REQUEST
    modifier onlyOwners(){
        bool owner = false;
        for(uint i=0; i<owners.length; i++){
            if(owners[i] == msg.sender){
                owner = true;
            }
        }
        require(owner == true, "Only wallet owner can create or approve a transfer request");
        _;
    }

/*    FUNCTIONALITY TO BE ADDED LATER ~dwb
        //ENSURE NO DULPLICATE OWNER ADDRESSES
    modifier uniqueOwners(){    
        bool uniqueOwner = true;
        for(uint x=0; x<=owners.length; x++){
            if(owners[x] == owners[x--]){
                uniqueOwner = false;
            }
        }
        require(uniqueOwner == true, "Duplicate owner addresses not permitted in contract deployment");
    }    
*/

        //INITIALIZE WALLET OWNERS LIST & NUMBER OF OWNERS REQUIRED TO APPROVE TRANSFER 
    constructor(address[] memory _owners, uint _minimumApprovals) {
        owners = _owners;
        minimumApprovals = _minimumApprovals;
    }   

        //SIMPLE FUNCTION FOR DEPOSIT OF FUNDS INTO WALLET
    function deposit() public payable {}

        //CREATE INSTANCE OF TRANSFER DATA STRUCT & ADD IT TO ARRAY OF transferRequests
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        
            //DISPLAY EVENT LOG OF TRANSFER REQUEST INFO
        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);
        
            //AMOUNT OF TRANSFER, RECIPIENT ADDRESS, # OF APPROVALS INITIALIZE AT 0, APPROVED OR NOT, ID WITHIN ARRAY
        transferRequests.push(Transfer(_amount, _receiver, 0, false, transferRequests.length));
    }

        //FUNCTION TO APPROVAL TRANSFER REQUEST
    function approve(uint _id) public onlyOwners {
  
            //OWNER CANNOT APPROVE THE SAME REQUEST TWICE
        require(approvals[msg.sender][_id] == false, "A wallet owner cannot approve same request twice");
        
            //OWNER CANNOT APPROVE A TRANSFER REQ THAT IS ALREADY SENT
        require(transferRequests[_id].hasBeenSent == false, "This transfer has already been sent");

            //SET APPROVAL FOR TRANSFER REQUEST & UPDATE MAPPING OF NUMBER OF APPROVALS FOR SPECIFIC REQUEST ID
        approvals[msg.sender][_id] = true;
        transferRequests[_id].approvals++;

            //LOG THE APPROVAL DETAILS
        emit ApprovalReceived(_id, transferRequests[_id].approvals, msg.sender);

            //SEND TRANSFER WHEN MINIMUM # OF APPROVALS IS REACHED
        if(transferRequests[_id].approvals >= minimumApprovals){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].recipient.transfer(transferRequests[_id].amount);
            emit TransferApproved(_id);
        }
    }

        //RETURN TRANSFER REQUESTS STRUCT DETAILS
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    }

/*    FUNCTIONALITY TO BE ADDED LATER ~dwb
        //get user balance
    function getUserBalance() public view returns(uint) {
        return balance[msg.sender]; 
    }
*/

        //DISPLAY CONTRACT BALANCE
    function getContractBalance() public view returns(uint) {
        return address(this).balance; 
    }
    
}
1 Like

Thank you very much, I used this instead to help me with the project :slight_smile:

1 Like

@jon_m

Hi, this is the code for my project, I spent a lot of time on it to get it right aha! Please check it looks good and do you think, perhaps after adding a few tweaks that I should upload it onto my github? Or do you think it’s not good enough to impress prospective employers?

Thanks!
Ismail

pragma solidity 0.7.5;

pragma abicoder v2;

contract Wallet1 {

address[] owners;

uint limit;

uint walletBalance;









struct Transaction{

    address payable to;

    uint amount;

    uint txID;

}



Transaction[] transferRequests;



//mapping with two inputs

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



mapping(uint => Transaction) transactions;



constructor(address[] memory owners1, uint approvals1) {

    owners = owners1;

    limit = approvals1;

    

}



function createTransactionRequest(address payable to, uint amount) public {



 

  require (getBalance() >= amount, "deposit more ether brudda");



 Transaction memory transaction = Transaction(to, amount, transferRequests.length);
 
 

 transferRequests.push(transaction);

 transactions[transaction.txID] = transaction;

}

function getTransactionRequests() public view returns (Transaction[] memory) {

    return transferRequests;

}

function approveTransaction(uint txID) public {

    uint approved = 0;

     approvals[msg.sender][txID] = true;
    
      
     
     

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

        

          

         if (approvals[(owners[i])][txID] == true) {

             approved++;

         }

        

         if (approved >= limit) {

             //makeTheTransfer

             transactions[txID].to.transfer(transactions[txID].amount);

             walletBalance -= transactions[txID].amount;

             delete transferRequests[txID];

             approved = 0;

         }

     }

}

function deposit()  public payable {

  

   walletBalance += msg.value;

}

function getBalance() public view returns (uint) {

   return walletBalance;

}

function getNoOfOwners() public view returns (uint) {

 return owners.length;

}

function getOwnerAddress(uint index) public view returns (address) {

 return owners[index]; 

}

function getLimit() public view returns(uint) {
return limit;
}

}

Hello. This is my solution of multi sig wallet and my first smart contract ever. Coded without Filip hint videos during 3 evenings :slight_smile: Check please and may be you will find any errors. Should I continue to work and study in this direction?

pragma solidity ^0.8.0;

contract Wallet {
    
    uint256 internal walletBalance;
    uint8 internal numberOfApprovals;
    address internal mainOwner;
    address[] internal additionalOwners;
    
    // Transaction object
    struct Transaction {
        address from;
        address to;
        uint256 amount;
        uint8 approvals;
        address[] approvedFrom;
    }
    
    Transaction[] transactions; // array of transactions
    
    event NewDeposit(uint256 _amount);
    event NewWithdraw(uint256 _amount);
    
    modifier onlyOwners {
        require (_isOwner(msg.sender), "You're not the wallet owner.");
        _;
    }
    
    function _isOwner(address _sender) private view returns (bool) {
         if (_sender == mainOwner) {
             return true;
         }
        
        // Check additional owners
        for (uint8 i = 0; i < additionalOwners.length; i++) {
           if (additionalOwners[i] == _sender) {
            return true;
           }
        }
        
        return false;
    }
    
    function _isApprovedEarlier(uint256 _txId, address _sender) internal view returns (bool) {
        for (uint8 i = 0; i < transactions[_txId].approvedFrom.length; i++) {
            if (_sender == transactions[_txId].approvedFrom[i]) {
                return true;
            }
        }
        
        return false;
    }
    
    function _transfer(address _to, uint256 _amount) internal {
        require (_amount <= walletBalance, "Insufficent wallet balance.");
         
        uint256 balanceBeforeWithdraw = walletBalance;
        walletBalance -= _amount;
        
        payable(_to).transfer(_amount);
         
        assert(walletBalance == balanceBeforeWithdraw - _amount);
    }
}
pragma solidity ^0.8.0;

import "./Wallet.sol";

contract MultiSigWallet is Wallet {
    
     constructor(address[] memory _additionalOwners, uint8 _numberOfApprovals) {
        require (_numberOfApprovals <= _additionalOwners.length, "Approvals number more than the wallet owners.");

        mainOwner = msg.sender;
        numberOfApprovals = _numberOfApprovals;
        additionalOwners = _additionalOwners;
    }
    
    function deposit() public payable returns (uint256) {
        require (msg.value > 0, "Deposit must be greater than zero.");
        
        uint256 balanceBeforeDeposit = walletBalance;
        walletBalance += msg.value;
        
        assert(walletBalance == balanceBeforeDeposit + msg.value);
        emit NewDeposit(msg.value);
        
        return walletBalance;
    }
    
    function withdraw(address _to, uint256 _amount) public onlyOwners returns (Transaction memory) {
        require (_amount <= walletBalance, "Insufficent wallet balance.");
        
        // Create new transaction
        address[] memory approvedFrom;
        Transaction memory newTransaction = Transaction(msg.sender, _to, _amount, 0, approvedFrom);
        transactions.push(newTransaction);
        
        // Call approve method immediately if number of approvals was set to 0
        if (numberOfApprovals == 0) {
            approve(transactions.length - 1);
        }
        
        emit NewWithdraw(_amount);
        
        return newTransaction;
    }
    
    function approve(uint256 _txId) public onlyOwners returns (Transaction memory) {
        require(transactions[_txId].approvals < numberOfApprovals, "Transaction has already been approved and sent.");
        require(msg.sender != transactions[_txId].from && numberOfApprovals > 0, "Transaction need to be approved by another owner."); // prevent approve by myself
        
        // Prevent approvals duplication
        require(!_isApprovedEarlier(_txId, msg.sender), "Transaction has already been approved by this owner.");
        
        transactions[_txId].approvals++; // increase transaction approvals
        transactions[_txId].approvedFrom.push(msg.sender); // add transaction approver
        
        // Transfer eth if approvals number was reached the desired
        if (transactions[_txId].approvals >= numberOfApprovals) {
            _transfer(transactions[_txId].to, transactions[_txId].amount);
        }
        
        return transactions[_txId];
    }
    
    // Getters
    function getWalletBalance() public view returns (uint256) {
        return walletBalance;
    }
    
    function getTransactionsList() public view returns (Transaction[] memory) {
        return transactions;
    }
    
}

my solution appears below. I learned that a struct cannot contain a mapping (at least that’s what I believe) and that fact caused be to have to create a separate “mapping (address => bool)[] addressesOfApproversThatHaventApproved;” to keep track of approvers/signers that have approved, so we don’t let them approve a 2nd time.

pragma solidity 0.7.5;

import “./Ownable.sol”;

contract MultiSigWallet is Ownable
{
enum RequestState {UNKNOWN, PENDING, SENT, CANCELLED }

struct TransferRequest{
    uint id;
    uint amount;
    address destination;
    RequestState state;
    uint requiredNumberOfApprovals;
    uint currentNumberOfApprovals;
 }

TransferRequest[] transferRequests;
mapping (address => bool)[] addressesOfApproversThatHaventApproved;

uint defaultRequiredNumberOfApprovals;
// this is a list of addresses that is used as a template for valid approvers when creating new transferRequests
address[] defaultApprovalAddresses;

function setRequiredNumberOfApprovals(uint _numApprovals) public onlyOwner
{
    defaultRequiredNumberOfApprovals = _numApprovals;
}

function getRequiredNumberOfApprovals() public view onlyOwner returns(uint) 
{
    return defaultRequiredNumberOfApprovals;
}

function setApprovalAddresses( address[] memory _addresses) public onlyOwner
{
    defaultApprovalAddresses = _addresses;
}

function getApprovalAddresses() public view onlyOwner returns(address[] memory)  
{
    return defaultApprovalAddresses;
}

// attempts to cancel the request while it is in PENDING state and returns true if the request is in cancelled state at the end of the operation
function cancelTransferRequest(uint _requestId) public onlyOwner returns(bool)  
{
    TransferRequest storage theRequest = transferRequests[_requestId];
    
    if( theRequest.state == RequestState.PENDING )
    {
        theRequest.state = RequestState.CANCELLED;
    }
    
    return theRequest.state == RequestState.CANCELLED;
}

// I want this to return the new balance
function deposit() public payable returns (uint) 
{
    // I wonder if this returns the balance before or after the deposit is complete...
    return address(this).balance;
}

// returns the requestId of the newly created request
function createTransferRequest(uint _amount, address _destination) public returns (uint)
{
    uint newRequestId = transferRequests.length;
    
    //TransferRequest storage newRequest = transferRequests[newRequestId];
    transferRequests.push( TransferRequest(
        {
            id : newRequestId,
            amount : _amount,
            destination : _destination,
            state : RequestState.PENDING,
            requiredNumberOfApprovals : defaultRequiredNumberOfApprovals,
            currentNumberOfApprovals : 0
        }));
    TransferRequest storage newRequest = transferRequests[newRequestId];
    
    // set up all of the addresses that can approve this request in remainingApprovalAddresses
    bool senderIsOneOfDefaultAddresses;
    addressesOfApproversThatHaventApproved.push();
    for(uint i = 0; i < defaultApprovalAddresses.length; i++)
    {
        address thisAddress  = defaultApprovalAddresses[i];
        addressesOfApproversThatHaventApproved[newRequestId][thisAddress] = true;
        if(thisAddress == msg.sender)
            senderIsOneOfDefaultAddresses = true;
    }
    
    require(senderIsOneOfDefaultAddresses, "This Sender is not allowed to create requests.");
  
    return newRequest.id;
}

// notes the approval and sends the amount if the required number of approvals is reached
// returns the state of the request after the function is complete
function approveAndSend(uint _requestId) public returns (RequestState)
{
    require(_requestId < transferRequests.length);
    TransferRequest storage theRequest = transferRequests[_requestId];
    
    if( theRequest.state == RequestState.PENDING )
    {
        // it's possible to approve even if there's not enough money in the contract, but it won't be sent and the approval will be rolled back
        if(addressesOfApproversThatHaventApproved[_requestId][msg.sender])
        {
            // approved by an approver that has not yet approved this request
            theRequest.currentNumberOfApprovals += 1;
            addressesOfApproversThatHaventApproved[_requestId][msg.sender] = false; // mark this approver down as having approved this request so that this approver doesn't get to approve multiple times
        }
    }
    
    if( theRequest.currentNumberOfApprovals >= theRequest.requiredNumberOfApprovals )
    {
        theRequest.state = RequestState.SENT;
        payable(theRequest.destination).transfer(theRequest.amount);
    }
    
    return theRequest.state;
}

}

======================

pragma solidity 0.7.5;

contract Ownable {

address owner;


constructor() {
   owner = msg.sender;
}

 modifier onlyOwner  {
    require(msg.sender == owner);
    _;
}

}

1 Like

Hey @szaslavsky. Yeah using the approvals as a mapping is a good way to go. However what you can do is to use a double mapping that maps an address and an ID to a boolean to whether that user has approved so it would look like

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

That way when you push a transfer instance into your transferrequest array of transfer instances transferRequests tge double mapping allwos you to query whether and address for a certain index in the tranafer request array has approved that transfer.

For example say i create a transfer and it gets pushed into the transfer array. It wilp now have an ID of 0. This if i want to see if address 0x123… has approved this transfer of id 0 i would chexk using the double mapping using approvals[ 0x123...][ 0 ] this mapping will outputsl true or false depending on whether address 0x123… has approved the transfer woth ID of 0.

Hope this makes sense. Also chexk out many of the other solutions here there are many unqiue approachs you could also get rid of the mapping and include an array attribute in your struct and append addresses into it as they approve then run a for loop to check the array as a requierment at the start of the approve function. Howver this may not be the most efficient as it requires loops but its another approach none the less to get you thinking. well done though good observation with the mapping

Evan

1 Like

Not gonna lie, i’m completely stuck even after the second video on this multisig wallet :confused:
trying to refrain from watching the next two videos but it’s really tough!! Will keep going, hoping someone can explain this to me as if i was 10 years old! haha

Am i right in saying that some of the tasks required here have not been touched on in the course so far? (just curious if i’ve missed an important part and should go back.)

Also, i’m missing the large amound of reading materials that the Javascript course gave us… Any suggestions on some good reading materials to help increase knowledge and memory of the language while we’re doing the course?!

Thanks in advance and best regards!

If you know and understood the bank contract example covered in the tutorials, and the double mapping explained in the second video, that is everything you need to know. Start by thinking and planning your contract. I personally used a pen and paper for this to get my thoughts out, and then start coding whatever you know/can think of. I spent a 1-2 weeks on it before I watched the second video, so keep trying! Persistence is key

1 Like

@mcgrane5 or anyone out there to have a look at my code above?! Thanks :slight_smile:

1 Like

Hey @Random i will look into this later this afternoon for you

1 Like

@random. Hahahah yes pen and paper, back to basics. I do this a lot too especially for hard processes. Or even a word doc. Its also great for debugging isnt it. If your function is throwing a runtime error and you cant figure it out get out your pen and paper pick some initial conditions and step through line by line. Every time youpp figure out whrre the error is.

1 Like

Yess it’s crazy how often finding the error is more difficult than actually fixing it ahaha

And thank youu, look forward to it

I seem to be getting a parse error in my constructor but it seems the same to everyone else… any thoughts guys? :slight_smile:

templatemultisig.sol:41:5: ParserError: Expected pragma, import directive or contract/interface/library/struct/enum/constant/function definition. constructor (address[] memory _owners, uint _limit) { ^---------^

My contstructor is here

constructor (address[] memory _owners, uint _limit) {
        owners = _owners ;
        limit = _limit;
    }

Completely understand what they do and their logic, i think i may need to go over things once more just to really sink it all in and realize how to use them in practice.

Thanks for the words of advice :)!

hey @Random could you edit your code post and make sure that your entire contratc is in the code snippets its just hard to read in its current state