Project - Multisig Wallet

This is how far I made it till now without using the template and trying all on my own. At the moment I am a bit stuck. Also I get error messages, but only with solidity 0.8.9; If i use 0.7.5 the same errorws do not appear. Do some of you know what I did wrong? I´ll mark the error messages with comments inside the code. Thanks in advance for your feedback.

pragma solidity 0.8.9;

contract WalletProject{
    
    mapping(address => uint) balance;
    
    address payable public owner;
    
    modifier onlyOwner{
        require(msg.sender == owner);
        _;
    }
    
    //admin is defined
    address[] public admins;
   
    //Add an Admin to the adminArray
    function addAdmin(address _admin)public onlyOwner{
        admins.push(_admin);
    }
    
    //modifier for admins
    modifier onlyAdmin{
       require(msg.sender == admins);
         //Error; Operator == not compatible with this adress type.
        _;
    }
    
    //Admins can approve the transaction here 
    function approveTransaction() public onlyAdmin{
        
    }
    
    //modifer for approved transaction
    
    
    //Here are the functions for the regular Enduser.
    function deposit() public payable returns (uint){
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }
    //Withdraw would need an array to save the transaction history and then approve those transactions.
    function withdraw(uint amount) public payable returns (uint){
        require(balance[msg.sender] >= amount, "Error! Not enough ETH!");
        balance[msg.sender] -= amount;
        msg.sender.transfer(amount);
        //Error .transfer is not available.
        return balance[msg.sender];
    }
    
    function transfer(address recipient, uint amount) public {
        require(balance[msg.sender] >= amount, "Balance not sufficient");
        require(msg.sender != recipient, "Don't transfer money to yourself");
        
        uint previousSenderBalance = balance[msg.sender];
        
        balance[msg.sender] -= amount;
        balance[recipient] += amount;
        
        assert(balance[msg.sender] == previousSenderBalance - amount);
    }
    
}
1 Like

hey @PaulS96. i fixed you code. there was too errors firstly your modifier you cann not compare an address with an array you need write a loop and compare each array elemnt to msg.sender see modified code

  modifier onlyAdmin{
        
        bool hasBeenFound = false;
        for (uint i = 0; i < admins.length; i++) {
             
             if(admins[i] == msg.sender) {
                 
                 hasBeenFound = true;
                 break;
             }
        }
        
        require(hasBeenFound, "not owner");
      
         //Error; Operator == not compatible with this adress type.
        _;
    }

also if you want to transfer the address needs to be payable so wwrap msg.sender in payable() look below

 payable(msg.sender).transfer(amount);

great work but you still have much to do if you get stuck again be sure to posy. your modified code is below


pragma solidity 0.8.9;

contract WalletProject{
    
    mapping(address => uint) balance;
    
    address payable public owner;
    
    modifier onlyOwner{
        require(msg.sender == owner);
        _;
    }
    
    //admin is defined
    address[] public admins;
   
    //Add an Admin to the adminArray
    function addAdmin(address _admin)public onlyOwner{
        admins.push(_admin);
    }
    
    //modifier for admins
    modifier onlyAdmin{
        
        bool hasBeenFound = false;
        for (uint i = 0; i < admins.length; i++) {
             
             if(admins[i] == msg.sender) {
                 
                 hasBeenFound = true;
                 break;
             }
        }
        
        require(hasBeenFound, "not owner");
      
         //Error; Operator == not compatible with this adress type.
        _;
    }
    
    //Admins can approve the transaction here 
    function approveTransaction() public onlyAdmin{
        
    }
    
    //modifer for approved transaction
    
    
    //Here are the functions for the regular Enduser.
    function deposit() public payable returns (uint){
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }
    //Withdraw would need an array to save the transaction history and then approve those transactions.
    function withdraw(uint amount) public returns (uint){
        require(balance[msg.sender] >= amount, "Error! Not enough ETH!");
        balance[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
        //Error .transfer is not available.
        return balance[msg.sender];
    }
    
    function transfer(address recipient, uint amount) public {
        require(balance[msg.sender] >= amount, "Balance not sufficient");
        require(msg.sender != recipient, "Don't transfer money to yourself");
        
        uint previousSenderBalance = balance[msg.sender];
        
        balance[msg.sender] -= amount;
        balance[recipient] += amount;
        
        assert(balance[msg.sender] == previousSenderBalance - amount);
    }
    
}


1 Like

@mcgrane5
Thanks for your help.

1 Like

hey @PaulS96 sorry i wanted to write this aswell but i was fixing your code on a zoom so i had to be quick. basically i your code you are attempting it a good way but you need to change a few things. firstly create a transfer struct. this will be the premise of how you make transfers. then what you need is a create transfer function that takes in the receiver address, amount etc. this function will initialise a transfer using the transfer struct and append this transfer to an array of transfers. then you need to set up an approve function which lets owners approve the transfer requests. for this you will need a mapping to keep track of the what owner has approved what transfer. you do not currently have this mapping but u should add it. then you need to create a transfer function that actually transfers the funds when it has enough approvals.

I can see you really want to do this on your own as you have not looked at the video so i will chanllenge you. ithe code below has all of the function headings you need (this is a hint for you) but the function bodies are blank for you to fill. i have left commetns to help you. Try this yourself and if your stuck come back to me.

pragma solidity 0.8.9;

contract WalletProject{
    
    mapping(address => uint) balance;
    mapping(address => mapping(uint => bool)) approvals;
    uint transferId = 0;
    uint approvalLimit;
    
    struct Transfer {
        
        address sender;
        address payable receiver;
        uint amount;
        uint approvals;
        uint id;
    }
    
    Transfer[] transferRequests;
    address[] owners;
    
    constructor() {
        
        owners.push(msg.sender);
        approvalLimit = owners.length - 1;
    }
    
    
    //modifier for admins
    modifier onlyOwner{
        
        bool hasBeenFound = false;
        for (uint i = 0; i < owners.length; i++) {
             
             if(owners[i] == msg.sender) {
                 
                 hasBeenFound = true;
                 break;
             }
        }
        
        require(hasBeenFound, "not owner");
      
         //Error; Operator == not compatible with this adress type.
        _;
    }
    
    //Add an Admin to the adminArray
    function addAdmin(address _owner)public onlyOwner{
        
        for(uint i = 0; i < owners.length; i++) {
            
            if(owners[i] == _owner) {
                
                revert("cannot add duplicate owners");
            } 
        }
        owners.push(_owner);
        approvalLimit = owners.length - 1;
    }
    
   
    //Here are the functions for the regular Enduser.
    function deposit() public payable returns (uint){
        
        require(msg.value != 0, "cannot deposit nothing");
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }
    //Withdraw would need an array to save the transaction history and then approve those transactions.
    function withdraw(uint amount) public returns (uint){
        
        require(balance[msg.sender] >= amount, "Error! Not enough ETH!");
        balance[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
      
        return balance[msg.sender];
    }
    
   function createTransferRequest(address payable receiver, uint amount) public onlyOwner {
       
       //you need to require the balance of the person is enough
       
       //you then need to create an instance of the transfer struc
       //and push it into the trasnferRequest array. Hint you cannot
       //do this in one line google how to append struct instance to array in solidity
       
       //increment the transfer id varaible i declare at the top
   }
   
   function approveTransfer(uint id) public onlyOwner {
       
       //require that the approval mapping for msg.sender for the transfer id is false
       
       //set the approval mapping of msg.sender for the id to true
       
       //increment the num of approvals for the transfer. you can do this
       //by indexing the transferRequets array with the id we pass in and
       //call .approvals and increment this by 1
       
       //make an if statement such that if the approval limit is equal to the
       //current approvals of the transfer then trnasfter the funds using .transfer
       //remember the address needs to be payable but i have doen this for you
   }
    
}

I have also modified your base coded and added a few things such as the approvals mapping. I also added an approval limit which is always equal to one minus the length of the owners array. in the constructor i append msg.sender to the array. in the add owner function i make the requirement to prevent duplicate owners. I also made a global tarsnfer id for you that we increment in the create transfer function

1 Like

Here is my submission, please note it’s not working and I have no idea why. I have a feeling it has something to do with my array of structs Approvedkeys. The array isn’t actually filling out with the ApprovedKey structs, which doesn’t make sense to me because remix isn’t showing any errors in my syntax.

I’ve been at it for about 8 hours total trying to figure it out and unfortunately I just can’t.

Also this is completely my own attempt, I have not yet looked at any of the project assistance.

pragma solidity 0.7.5;

contract multiSigWallet{
    
    mapping(address => uint) balance;
    
    struct ApprovedKey{
        uint person;
        address myWalletAddress;
        bool signedIn;
    }
    
    ApprovedKey approvedKey1 = ApprovedKey(
      1,
      0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,
      false
    );
    ApprovedKey approvedKey2 = ApprovedKey(
      2,
      0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,
      false
    );
    ApprovedKey approvedKey3 = ApprovedKey(
      3,
      0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c,
      false
    );
    
    ApprovedKey[] ApprovedKeys;
    
    uint approvedKeysCheck = 0;
    string message = "there is no message";
    
    
    // Deposit money without sig
    function Deposit(uint _depositAmount) public payable returns (uint) {
       
        balance[msg.sender] = _depositAmount + balance[msg.sender];
        
        return balance[msg.sender];
    }
    
    
    // Transfer money - must check balance is valid and user is not sending money to themselves. Must also check for multi sig, at least 2 out of 3 approved signatures.
    function TransferMoney(uint _transferAmount, address _transferHere) public {
        require(balance[msg.sender] >= _transferAmount, "Insufficient funds, sorry lah");
        require(msg.sender != _transferHere, "you cannot send money to yourself!");
        require(approvedKeysCheck >= 2, "Sorry, your signature is either not signed in, or there needs to be more signatures before we can approve this transaction");

        balance[_transferHere] = _transferAmount + balance[_transferHere];
        
    }
        
    // Check balance of msg sender wallet
    function CheckBalance() view public returns (uint) {
        return balance[msg.sender];
    }
    
    
    // Multi Sig - Sign for transaction. Must include 2-3 signatures from the approvedSig mapping before successfully going through.
    
   function SignHere(address _signature) public  returns  (string memory){
        for(uint i = 0; i < ApprovedKeys.length; i++){
            if(_signature == ApprovedKeys[i].myWalletAddress){
            ApprovedKeys[i].signedIn == true;
            approvedKeysCheck += 1;
            message = "Your wallet is now signed in";
            }
            else if(ApprovedKeys[i].signedIn == true){
                message = "your wallet is already signed in";
            }
            else{
            message = "Your wallet is not approved";
            }
        }
        message = "it did not go through the loop";
        
        return message;
    }
 
    
    // record all transfers
    
}```

This is my second attempt at the wallet with help by @mcgrane5 . Currently I am stuck at the last function. Thanks in advance for telling me how I can improve my code.

pragma solidity 0.8.9;

contract WalletProject{
    
    mapping(address => uint) balance;
    mapping(address => mapping(uint => bool)) approvals;
    uint transferId = 0;
    uint approvalLimit;
    
    struct Transfer {
        
        address sender;
        address payable receiver;
        uint amount;
        uint approvals;
        uint id;
    }
    
    Transfer[] transferRequests;
    address[] owners;
    
    constructor() {
        
        owners.push(msg.sender);
        approvalLimit = owners.length - 1;
    }
    
    
    //modifier for admins
    modifier onlyOwner{
        
        bool hasBeenFound = false;
        for (uint i = 0; i < owners.length; i++) {
             
             if(owners[i] == msg.sender) {
                 
                 hasBeenFound = true;
                 break;
             }
        }
        require(hasBeenFound, "not owner");
        _;
    }
    
    //Add an Admin to the adminArray
    function addAdmin(address _owner)public onlyOwner{
        
        for(uint i = 0; i < owners.length; i++) {
            
            if(owners[i] == _owner) {
                
                revert("cannot add duplicate owners");
            } 
        }
        owners.push(_owner);
        approvalLimit = owners.length - 1;
    }
    
   
    //Here are the functions for the regular Enduser.
    function deposit() public payable returns (uint){
        
        require(msg.value != 0, "cannot deposit nothing");
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }
   
    function withdraw(uint amount) public returns (uint){
        
        require(balance[msg.sender] >= amount, "Error! Not enough ETH!");
        balance[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
      
        return balance[msg.sender];
    }
    
    function createTransferRequest(address payable _receiver, uint _amount) public onlyOwner{
       
        require(balance[msg.sender] >= _amount, "Balance not sufficient");
        require(msg.sender != _receiver, "Don't transfer money to yourself");
        
        Transfer memory transferInstance = Transfer(msg.sender, _receiver, _amount, approvalLimit, transferId);
       
        transferRequests.push(transferInstance);
        transferId ++;
    }
   
    function approveTransfer(uint id) public onlyOwner {
       
        //require that the approval mapping for msg.sender for the transfer id is false
        require(approvals[msg.sender][transferId] == false, "Transfer already authorized!");
        //set the approval mapping of msg.sender for the id to true
        approvals[msg.sender][transferId] == true;
        //increment the num of approvals for the transfer. you can do this
        //by indexing the transferRequest array with the id we pass in and
        //call .approvals and increment this by 1
        transferRequests[id].approvals ++;
        //make an if statement such that if the approval limit is equal to the
        //current approvals of the transfer then trnasfter the funds using .transfer
        //remember the address needs to be payable but i have doen this for you
        if (transferRequests[id].approvals ++ == approvalLimit){
            uint previousSenderBalance = balance[msg.sender];
        
            balance[msg.sender] -= transferRequests.amount;
            balance[transferRequests._receiver] += transferRequests._amount;
            msg.sender.transfer(transferRequests.amount);
    
            assert(balance[msg.sender] == previousSenderBalance - createTransferRequest.amount);
            return("Transaction approved!");
        }
        else{
            return("Not approved!");
        }
    }
}
1 Like

hry @PaulS96 you have probably well looked the code by this stage. sorry i fogot to highlight the fixes. basically most of them were in the approve function at the bottom in the case where you are trying to transfer the funds when the limit has been reached. you were saying

transferRequests.id .... etc

instead of

transferRequests[_id].id  //gets the id of a specific transfer whos id is _id (see createTransfer)

remember transferRequests is an array therefore if we wsnt to access its elements we need to index it. thus we use _id. There were a few other little syntac things such as

require(approvals[msg.sender][_id] == false;

approvals[msg.sender][_id] == true //this shoul be = true not == true

it was very baby thing slike these but the more you use solidity the lesss this will happen trust me it took me a while too in the beginning. But very well done man amazing work see you around the forums.

1 Like

hry @PaulS96 sorry for being late getting back.as i said i did not have to do much fixes but some of them are explained below

transferRequests.id .... etc

instead of

transferRequests[_id].id  //gets the id of a specific transfer whos id is _id (see createTransfer)

remember transferRequests is an array therefore if we wsnt to access its elements we need to index it. thus we use _id. There were a few other little syntac things such as

require(approvals[msg.sender][_id] == false;

approvals[msg.sender][_id] == true //this shoul be = true not == true

it was very baby thing slike these but the more you use solidity the lesss this will happen trust me it took me a while too in the beginning. But very well done man amazing work see you around the forums.

2 Likes

Thanks @mcgrane5 for your help and your feedback.

Hello everybody :slight_smile:

Here comes multisigwallet. I believe it covers all the requirements and I have not been able to “hack” it yet. It was a fun assignment and a cool course overall. Perhaps someone can tell me how to improve or adapt the code if I have fallen into any pitfalls. Some helperfunctions are only for me to determine whether or not code is working and eth actually being sent etc.

pragma solidity 0.7.5;
pragma abicoder v2;

contract MultiSigWallet{
address payable public creator;

address[] owners;
uint numberOfApprovals;
mapping(address => mapping(uint => bool)) approvals;

Transfer[] transferRequests;

struct Transfer {
    uint transferID;
    address payable to;
    uint amount;
    uint approvals;
    bool sent;
}

constructor (address firstOwner, address secondOwner, address thirdOwner, uint requiredApprovals) {
    owners.push(firstOwner);
    owners.push(secondOwner);
    owners.push(thirdOwner);
    numberOfApprovals = requiredApprovals;
    
    creator = msg.sender;
}

//Helper function to lookup msg.sender against the array of owners
function validRequester(address requester) internal view returns (bool) {
    for (uint i=0; i<owners.length; i++) {
        if(owners[i] == requester){
            return true;
        }
    }
    return false;
}

//rransferrequest is submitted and returns true if it passes require statements and transfer request is created
function createTransferRequest(address payable to, uint amount) public returns (bool) {
    require(address(this).balance >= amount, "Not enough funds to request this transaction");
    require(validRequester(msg.sender), "You shall not pass!");
    
    //approvals set to one since the requester is also an approver for usability
    Transfer memory newTransfer = Transfer(transferRequests.length, to, amount, 1, false);
    transferRequests.push(newTransfer);
    approvals[msg.sender][transferRequests.length-1]=true;
    return true;

}


function approveTransferRequest(uint transferID) public {
    require(validRequester(msg.sender), "You shall not pass!");
    
    if(approvals[msg.sender][transferID]!=true) {
        approvals[msg.sender][transferID]=true;
        transferRequests[transferID].approvals += 1;
        if(transferRequests[transferID].approvals >= numberOfApprovals && transferRequests[transferID].sent != true) {
            transfer(transferRequests[transferID].to, transferRequests[transferID].amount);
            transferRequests[transferID].sent = true;
        }
    }
}

function transfer(address payable recipient, uint amount) private {
    require(address(this).balance >= amount, "Balance not sufficient");
    bool sent = recipient.send(amount);
    require(sent, "Failed to send Ether");
}

function getRequiredApprovals() public view returns (uint) {
    return numberOfApprovals;
}

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


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

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

function deposit() public payable returns (uint) {
    return address(this).balance;
}

modifier onlyCreator {
    require(msg.sender == creator);
    _;
}

}

1 Like

This was an interesting task, feedback is more than welcome.

pragma solidity 0.7.5;
pragma abicoder v2;

contract MultiSigWallet {
      
   event depositComplete(uint amount, address indexed sender);
   event requestPending(uint amount, address indexed sender, address indexed recipient);
   event transactionSent(uint amount, address indexed recipient);
   
     address[] allOwners;
     uint balance;
     uint ConfirmationsRequired;
     mapping(address => bool) checkOwner;
     mapping(uint => mapping(address => bool)) isConfirmed;
     
     struct Transfer {
     address payable recipient;
     uint amount;
     uint transferId;
     uint confirmationCount;
     bool happened;
        }
        
     modifier onlyOwner() {
         bool owner = false;
         for(uint i = 0; i < allOwners.length;i++){
         if(allOwners[i] == msg.sender){
             owner = true;
             
          
            
        
         _;
         }
     }
        require(owner == true, "Only owners");
     }
     
     modifier notConfirmed(uint transferId) {
        require(!isConfirmed[transferId][msg.sender], "Transaction already confirmed");
        _;
    }
     
     constructor(address[] memory _allOwners, uint _ConfirmationsRequired){
           allOwners = _allOwners;
           ConfirmationsRequired = _ConfirmationsRequired;
    
     }

     
     function transactionRequest(address payable recipient, uint amount) public payable {
         require(balance >= amount);
         trRequests.push(Transfer(recipient,amount,0,0,false));
         emit requestPending(msg.value, msg.sender, recipient);
     
     }
     function signRequest(uint transferId) public onlyOwner {
    
         Transfer storage transfer = trRequests[transferId];
         transfer.confirmationCount += 1;
         isConfirmed[transferId][msg.sender] = true;
         
        if(transfer.confirmationCount >= ConfirmationsRequired){
            trRequests[transferId].recipient.transfer(trRequests[transferId].amount); 
            balance = balance - trRequests[transferId].amount;
         emit transactionSent(trRequests[transferId].amount, trRequests[transferId].recipient);
     }
     }

    Transfer[] trRequests;
    
    function deposit() public payable returns (uint) {
        balance += msg.value;
        emit depositComplete(msg.value, msg.sender);
        return balance;
    }
    
    function currentBalance() public view returns (uint) {
        return balance;
    }

    function minimumSigns() public view returns (uint) {
    return ConfirmationsRequired;
    }
    function TotalRequestedTransfers() public view returns (uint) {
    return trRequests.length;    
    }
}

Hey @jon_m, i have yet to try out your script because i’m trying to finish up asap, but i will after i finish the multisig wallet project.
while we’re on this topic of particular solidity moves that are allowed; do you know of a way to refer to / return / input ranges in arrays/structs? for the multisig wallet, i’m trying to assign the array of initially allowed addresses to an address array… i’ve seen colon elsewhere, .e.g. array(1:5), and maybe this is akin to the rest parameter in javascript??
my code for that:

constructor (address[] memory _contributors, uint _quorum) {
        contributors[:] = _contributors[:];
    }

anyway, i found a lot of moves that don’t translate between other languages and solidity; that was frustrating to code this project, but i’m staying open to the utility of having less flexible syntax. that said, i wish we’d gotten a bit more of a straightforward hint from filip, especially with respect to the use (or NOT! haha) of tuples and ranges or e.g. this quote i found on stackexchange (at https://ethereum.meta.stackexchange.com/questions/414/introducing-ethereum-blog-overflow/419#419):

A few tips: if possible think in mappings, not arrays. If you’re coding locally, make sure you are mining on your testnet, otherwise nothing will happen. It’s usually better to use an IDE like ether.camp for proof of concepts. If you can, avoid strings and especially floating point numbers. Define a denominator and discretize your floating point numbers.
Symeof
Feb 10 '17 at 11:26
…kind of best practices (which i realize may change often…). Probably most helpful (to me or others with experience who know enough to hurt themselves trying) would be a hierarchy or cladogram of language names that traces the origins of solidity in terms of languages from which it has borrowed most heavily. anyway, food for thought, and thanks for reading.

here’s my latest code to troubleshoot (i’m giving it another day, and after then i’ll just need to copy the answer from filip because life is too short for me to guess and check all of the vagaries between programming languages):

    function requestXferVote (string memory _rationale, address _to, uint _toXfer) public onlyContributors {
//        string memory done = "active voting: your vote added";
        for (i=0; i<=vIni.length+1; i+1){
            if (i > vIni.length) {
                vIni.contributor.push(msg.sender);
                vIni.receiver.push(_to);
                vIni.amt.push(_toXfer);
                vIni.ayevote.push(1);
                vIni.requestID.push(_rationale);
            }

the problem above is that for each entry below the if header, i’m getting the following error: “member not found or not visible after argument-dependent lookup in struct”, which stays the same when i try using a for loop and with an indexer to try and update each one independently, e.g.

        for (i=0; i<=vIni.length+1; i+1){
            if (i > vIni.length) {
                uint a;
                for (a=0; a<vIni.length; a++) {
                    vIni.contributor[a].push(msg.sender);
                    vIni.receiver[a].push(_to);
                    vIni.amt[a].push(_toXfer);
                    vIni.ayevote[a].push(1);
                    vIni.requestID[a].push(_rationale);
                }
            }

the struct i’m referencing is established above all that at

    struct voterInitiatives { //the thing carrying all the data about each xfer request: who requested, to whom and how much, number of aye votes, description
        address[] contributor;
        address[] receiver;
        uint[] amt;
        uint[] ayevote;
        string[] requestID;
    }
    voterInitiatives[] vIni;

the closest to a solution that i can find is in this answer to a similar question (about pushing AND retrieving):
https://ethereum.stackexchange.com/questions/63493/how-do-i-read-an-array-inside-of-a-struct#63494

1 Like

Hello!
My wallet is not working. Please take a look and let me know if you find any errors that I might not be able to see on my end. Much appreciate🤩

pragma solidity 0.7.5;
pragma abicoder v2;

//["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"]

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;
    mapping(address => uint) balance;
    
//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(owners[i] == msg.sender){
           owner = true;
           }
      }
      require(owner == true);
      _;
   }
  
//should initialize the owners list and the limit of approvals
  constructor (address[] memory _owners, uint _limit) {
      owners = _owners;
      limit = _limit;
      
  }
  event TransferRequestCreated (address _from, address _to, uint _amount);
  
  //Deposit function stays empty, anyone can Deposit no need to execute this logic
  function deposit() public payable {
      balance[msg.sender] += msg.value;
  }
  
    //show balance
    function showBalance() view public returns (uint) {
        return balance[msg.sender];
    }
  
  //transfer function - a request to transfer out money from the mulitsig wallet
  //the id is 0 cause the length of the array is 0 
  function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
      Transfer(_amount, _receiver, 0, false, transferRequests.length);
      transferRequests.push(
          Transfer(_amount, _receiver, 0, false, transferRequests.length));
      emit TransferRequestCreated(msg.sender, _receiver, _amount);
}
  
  //Set your approval for one of the transfer reqquests
  //Need to update the Transfer object
  //Need to update the mapping to record the approval for the msg.sender
  //When the amount of approval 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 transfer request that has already be sent 
 function approve(uint _id) public onlyOwners {
     require(transferRequests[_id] .hasBeenSent ==false);
     require(approvals[msg.sender] [_id] == false);
     
     approvals[msg.sender] [_id] = true;
     transferRequests [_id] .approvals++;
     
    
     
     if(transferRequests [_id].approvals >= limit){
         transferRequests[_id].hasBeenSent = true;
         transferRequests[_id].receiver.transfer(transferRequests [_id] .amount);
     }
     
 }
 
 //return all transfer reqquests
   function getTransferRequests() public view returns (uint) {
       return transferRequests.length;
   }
 }
1 Like

nesting complex data types needs to have a lesson on its own… with some practice modules. not just a double mapping, but the errors we’ll run into when we try a struct/array/mapping of structs/arrays/mappings of structs/arrays/mappings…and so on… navigating these was not intuitive, and i’m not even sure i exhausted all possibilities before finally giving up.

that said, here’s the code i came up with before cheating and looking at filip’s answers:

pragma solidity 0.7.5;
pragma abicoder v2;  //helpful because it allows RETURNing of arrays/structs/i.e. tuples
contract mswallet {
//state variables 
    struct voterInitiatives { //the thing carrying all the data about each xfer request: who requested, to whom and how much, number of aye votes, description
        address payable[] contributor;
        address payable [] receiver;
        uint[] amt;
        uint[] ayevote;
        string[] requestID;
    }
    voterInitiatives vIni;
    address[] contributors;
    uint balance = address(this).balance;
    uint quorum;
    uint i=0; //generic counter
//just storing user input in state variables for later reference
    constructor (address[] memory _contributors, uint _quorum) payable {
        contributors = _contributors;
        quorum = _quorum;
        balance = msg.value; 
    }
//permission requests for xfers  
    modifier onlyContributors() {
        require (checkContributor(),"only contributors can do that");
        _; 
    }
            function checkContributor() private returns(bool){
                bool c;
                if (msg.sender == contributors[i]) {return c=true;}
                else if (i < contributors.length) {i+1; return checkContributor();}
                else return c=false;
            }
//any one on entire blockchain can deposit
    function addToPool() public payable returns(uint){
        balance += msg.value;
        return address(this).balance;
    }
//check which transfers are available on which to vote
    function listXfers() public view returns (voterInitiatives memory) {
        return vIni;
    }
//contributors can request a vote to transfer funds from this contract by naming their vote, giving an address and an amount to be sent (can also be used to vote aye if it's already in there)
    function requestXferVote (string memory _rationale, address payable _to, uint _toXfer) public onlyContributors {
        voterInitiatives storage local_vIni = vIni;
        local_vIni.receiver.push(_to);
        local_vIni.amt.push(_toXfer);
        local_vIni.requestID.push(_rationale);
        vIni=local_vIni;
    }
        //string comparison function i got from https://ethereum.stackexchange.com/questions/30912/how-to-compare-strings-in-solidity
            function compareStrings (string memory a, string memory b) private pure returns (bool) {
                return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b))));
            }
//vote! (and get back confirmation if this was last required vote)
    function voteAye(uint _requestIdx) public onlyContributors returns (string memory) {
        vIni.ayevote[_requestIdx]+1; 
        return checkXfervote(_requestIdx);
    }
//check if there's an aye majority
    function checkXfervote(uint _index) private returns (string memory) {
        if (vIni.ayevote[_index] >= quorum) {
            initiateXfer(vIni.receiver[_index], vIni.amt[_index]);
            return ("validated...transferring...");
        }
        else return '("ayevotes still < quorum")';
    }
//execute the xfer
    function initiateXfer(address payable _to, uint _amt) private {
        //address(this).balance -= _amt;
        _to.transfer(_amt);
    }
}```

compiled without errors or warnings, then deployed fine, then i did not test in depth because i'm not in a production environment, and it's sunday night where i have to get other things done that i didn't do during the past few days... so thankful i finally figured this out!

major problem was how to access/set/get/etc the arrays within the database struct, 'vIni'. major compiler error i overcame is listed in first response above about an unfound member:  overcame by using a local storage pointer to 'vIni' called 'local_vIni', although unsure how many compiler updates that will survive or even if it works exactly as intended because i didn't test thoroughly... warning: there may be some recursive code not resolving well...i'd be embarrassed, but i haven't yet taken classes on solidity DEBUGGING, and have to move on now!

was this from the end of the video? he goes over some additional points throughout the video; if not, try from there and see it it’s still not working…

First attempt from the project definition. Seems to be working.

pragma solidity ^0.7.5;


contract MultiSig {
    
   
    mapping(address => uint) owners_map;
    uint numApprovals;
    
    struct TransferRequest{
        address payable to;
        uint amount;
        mapping(address => uint) voted;
        uint totalvotes;}
        
    TransferRequest[] public requests;
    
    modifier onlyAnOwner() {
    require(owners_map[msg.sender] == 1); 
    _;}
    
    
    constructor(address[] memory _owners, uint _numApprovals) {
        require(_numApprovals <= _owners.length, 'too many approvals required'); 
        numApprovals = _numApprovals;

        for(uint i = 0; i < _owners.length; i++) 
        {owners_map[_owners[i]] = 1;}
        }
    
    function deposit() public payable {}
    
    
    function getContractBalance() public view returns (uint) {
        return address(this).balance;}
    
    // initially code couldn't push with mapping type
    //found workaround https://ethereum.stackexchange.com/questions/97366
    function createTransferRequest(address payable _to, uint _amount) public payable onlyAnOwner {
        uint256 idx = requests.length;
        requests.push();
        TransferRequest storage t = requests[idx];
        t.to = _to;
        t.amount = _amount;
        }
    
    function voteForRequest(uint _request) public onlyAnOwner {
    TransferRequest storage t = requests[_request];
    require (t.voted[msg.sender] == 0, 'already voted');
    t.voted[msg.sender] += 1;
    t.totalvotes += 1;
    
    if (t.totalvotes >= numApprovals) 
        {t.to.transfer(t.amount);
        t.amount = 0; //reset amount to avoid double sending
        }
    }
    
}
1 Like

Thank you! @B_S - Yes, it’s something in the end that is not working for me, but I will continue at it🤩

This is my second attempt at the Multi sig wallet. I wrote this without looking at any hints or suggestions so hopefully the wallet does what it’s supposed to.

My multi sig wallet includes the following functions:
1 - Deposit - Can deposit without multiple signatures (this makes sense to me because people can send money to bank accounts without getting approval from the bank account owner, all they need are the bank account details to send money)
2 - Transfer money - This function requires that you set an amount to transfer, and the address to send the amount to
3 - Sign Here - This is the function I had the most difficult time with. It essentially enables you to enter wallet addresses. The addressed then get checked to see if they are on the approved list for signing the transaction, if they are on the list, then the address gets signed in. If the address has already signed in it will also notify you that it has been signed in already. There are 3 addresses on the approved list. I initially tried to write this function using a loop to iterate through a mapping, then an array, and I couldn’t figure out how to make either one work, so I resorted to some very basic, nested if else statements to give me the functionality I was trying to create. As a result there were more lines of code then I would have liked, but at least it works in the end.
4 - Check Balance - checks balance of msg.sender

Anyways would love to get some feedback on this:

pragma solidity 0.7.5;

contract multiSigWallet{
    
    mapping(address => uint) balance;
    
    struct ApprovedKey{
        uint person;
        address myWalletAddress;
        bool signedIn;
    }
    
    ApprovedKey approvedKey1 = ApprovedKey(
      1,
      0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,
      false
    );
    ApprovedKey approvedKey2 = ApprovedKey(
      2,
      0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,
      false
    );
    ApprovedKey approvedKey3 = ApprovedKey(
      3,
      0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c,
      false
    );
    
    uint approvedKeysCheck = 0;
    string message = "there is no message";
    
    
    // Deposit money without sig
    function Deposit(uint _depositAmount) public payable returns (uint) {
       
        balance[msg.sender] = _depositAmount + balance[msg.sender];
        
        return balance[msg.sender];
    }
    
    
    // Transfer money - must check balance is valid and user is not sending money to themselves. Must also check for multi sig, at least 2 out of 3 approved signatures.
    function TransferMoney(uint _transferAmount, address _transferHere) public {
        require(balance[msg.sender] >= _transferAmount, "Insufficient funds, sorry lah");
        require(msg.sender != _transferHere, "you cannot send money to yourself!");
        require(approvedKeysCheck >= 2, "Sorry, your signature is either not signed in, or there needs to be more signatures before we can approve this transaction");

        balance[_transferHere] = _transferAmount + balance[_transferHere];
        
        assert(balance[msg.sender] == balance[msg.sender] - _transferAmount);
        
    }
        
    // Check balance of msg sender wallet
    function CheckBalance() view public returns (uint) {
        return balance[msg.sender];
    }
    
    
    // Multi Sig - Sign for transaction. Must include 2-3 signatures from the approvedSig mapping before successfully going through.
    
    function SignHere(address _signature) public  returns  (string memory, uint, bool){
                if(_signature == approvedKey1.myWalletAddress){
                    if(approvedKey1.signedIn == true){
                        message = "Key 1 is already signed in";
                    }
                    else if(approvedKey1.signedIn == false){
                    approvedKey1.signedIn = true;
                    approvedKeysCheck += 1;
                    message = "Approved Key 1 accepted. Your wallet is now signed in";
                    }
                }
                else if(_signature == approvedKey2.myWalletAddress){
                    if(approvedKey2.signedIn == true){
                        message = "Key 2 is already signed in";
                    }
                    else if(approvedKey2.signedIn == false){
                    approvedKey2.signedIn = true;
                    approvedKeysCheck += 1;
                    message = "Approved Key 2 accepted. Your wallet is now signed in";
                    }
                }
                else if(_signature == approvedKey3.myWalletAddress){
                    if(approvedKey3.signedIn == true){
                        message = "Key 3 is already signed in";
                    }
                    else if(approvedKey3.signedIn == false){
                    approvedKey3.signedIn = true;
                    approvedKeysCheck += 1;
                    message = "Approved Key 3 accepted. Your wallet is now signed in";
                    }
                }
                else{
                    message = "it went through the else statement";
                   
                }
                 return (message,approvedKeysCheck,approvedKey1.signedIn);
    }
 
    
    // record all transfers
    
}

Hi @B_S,

I’m not sure I fully understand what you’re trying to do, here. There is no point passing an array to a constructor if you don’t need to immediately assign all of the values in that array to some kind of state variable. Anything that isn’t assigned within the constructor will be lost when the constructor finishes executing. If you only have a few addresses, and you want to assign them to different storage arrays, then you can pass them as separate address arguments to the constructor, and then push or assign each address separately, depending on whether your storage arrays are dynamically-sized (push) or fixed-size (assign by index).

If you are inputting a large array of addresses, my first question would be, do they all need to be assigned to storage on deployment? Can’t they be added individually via a function after deployment? If you do need to call the constructor with an array of addresses, then the whole array can be assigned to your storage array variable e.g.

address[] contributors;

constructor(address[6] memory _contributors) {
    contributors = _contributors;
}

In this example, on deployment I’m assigning a fixed array of 6 addresses to a dynamically-sized storage array. This means that my storage array will initially contain 6 addresses, and an unlimited number of additional addresses can then be added individually at any time afterwards.

If you need to assign different ranges of addresses to more than one storage array, then the simplest way to do this is to call the constructor with these ranges already allocated to separate arrays e.g.

address[5] contributorsA;
address[5] contributorsB;

constructor(address[3] memory _contributorsA, address[5] memory _contributorsB {
    contributorsA = _contributorsA;
    contributorsB = _contributorsB;
}

In this example, on deployment I’m assigning a fixed-size array of 3 addresses to a fixed-size storage array of 5 addresses, and a second fixed-size array of 5 addresses to another fixed-size storage array of 5 addresses. This means that one of my storage arrays will initially contain 3 addresses, and there is space for 2 more to be assigned to indices 3 and 4 at any time afterwards; but the other storage array will initially contain 5 addresses, with no capacity for any additional addresses to be added. However, any of the addresses could be replaced by different ones, if necessary.

If at some stage after deployment, I wanted to retrieve some values from a storage array and return them as a separate array, then, in a separate function I would iterate over the storage array to retrieve the values I wanted to extract, adding them one by one to a memory array, and then return this new array once the for-loop had finished iterating. However, a word of warning: values cannot be pushed to a memory array, only assigned to a specified index, and the length of the memory array needs to be defined beforehand as either (i) a fixed-size memory array, or (ii) a dynamic-length memory array using the new keyword. This is a complex area and would require a certain amount of research, and probably best undertaken after you have more experience under your belt.

But any sorting, complex manipulation, or heavy lifting of data that can be done in the front end is better done there instead of within the smart contract, in order to avoid unnecessarily high gas costs.

I get the feeling you are trying to overcomplicate your smart contract. With smart contracts we need to keep operations as simple and as streamlined as possible. When the code becomes overly complex, there is a greater risk of bugs, vulnerabilities and attacks.

1 Like

A problem here seems to be that you are trying to reference properties of an array (which isn’t possible)…

vIni is an array of struct instances. It’s the struct instances stored within this array which will have the properties contributor, receiver etc.
So, you would first need to reference the struct instance within the array (by its index) and then reference the property by appending it to the struct instance with dot notation e.g.

for (uint i = 0; a < vIni.length; i++) {
    vIni[i].contributor.push(msg.sender);
    // etc.
}

Also, the if-statement condition doesn’t make any sense…

i will never be greater than vIni.length because the loop will have finished iterating by then.

My advice is to start off with a project without such complicated data structures :wink:

I’ll leave you now in the capable hands of @thecil and @mcgrane5 who are the expert reviewers of this Multisig Wallet project… I just wanted to reply to your specific questions, as you’d addressed them to me :muscle: :smiley:

3 Likes