Project - Multisig Wallet

There is no specific reason rather than your own logic (in videos, filip just do it that way because he think thats good for what he need), so the event could go in any place you want/need to, BUT taking some considerations in the way blockchain works, maybe the event should be emitted once the function execution has completed properly.

Example:

function doSomething() public view returns(bool){
emit functionTriggered("some message");
require(msg.sender != owner, "owner cant call this function");
}

In this code, the event will be executed before the require statement, so maybe in this case, is better to put the event after the require.

Is just what it came to his mind in that moment, but off course you can add as much parameters you think will give you more transparency. :nerd_face:

Overall, nice work in the contract! :partying_face:

Carlos Z

Hey @JosephGarza, hope you are ok.

The only error that i see with your contract is that you need to add an extra } at the end of the contract to close the contract logic properly, then the contract is compiled properly and ready for deployment.

Carlos Z

Hey @Anvar_Xadja, hope you are great.

Could you also please share your ownable contract? which i need for some of your functions.

Carlos Z

Both contracts have a constructor right?
multiSig is Ownable so the parent contract is multiSig which will have the constructor to initialize properly all the childs contracts.

Carlos Z

@thecil Can you address the topics that I brought to your attention, please?

OPEN QUOTE I have made progress using a different tutorial. I’ve come across some issues with my code. Could I please get a hand here?
I understand that for starters, the nested struct is not supported in the newer version of Solidity - at least not in the 0.7.5 version. Could I please get some pointers? CLOSE QUOTE

Specifically, what I would like to know is how to approach the fact that nested structs are not supported in this version of Solidity. How is it that I should go about coding this? Could you please help with this?

Thanks in advance.

Here is my updated code from Wallet.sol and Ownable.sol

Wallet.sol

/*
Arrays of accounts that are owners = ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]
Sending ether to this account = "0x617F2E2fD72FD9D5503197092aC168c91465E7f2"
amount = 1000000000000000000
*/

pragma solidity 0.7.5;
pragma abicoder v2;

import "./Ownable.sol";

contract MultisigWallet is Ownable {
    
    /*
     *
     Events
     *
     */    
    event depositDone(address indexed sender, uint amount, uint balance);
    event ownersDone(address indexed sender);
    event submitTransactionDone(address indexed sender, uint indexed id, address indexed to, uint amount);
    event approved(address indexed sender, uint indexed txId);
    event executeTransferDone(address indexed sender, uint amount);
    
    uint limit;
    mapping(address => mapping(uint => bool)) approvals;

    struct Transfer {
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }
    
    Transfer[] transfer;
    
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    /*
     *
     Modifiers
     *
     */
    
    modifier txExist(uint _id) {
        require(_id < transfer.length, "Transaction does not exist");
        _;
    }
    
    modifier notExecuted(uint _id) {
        require(!transfer[_id].hasBeenSent, "Transaction already confirmed");
        _;
    }
    
    /*
     *
     Functions
     *
     */
    
    function deposit() payable external {
        emit depositDone(msg.sender, msg.value, address(this).balance);
    }
    
    function getBalance() public returns(uint) {
        return (address(this).balance);
    }
    
    function submitTransaction(address payable _to, uint _amount) public onlyOwners {
        transfer.push(Transfer(_amount, _to, 0, false, transfer.length));
        emit submitTransactionDone(msg.sender, transfer.length, _to, _amount);
    }
    
    function approve(uint _id) public onlyOwners txExist(_id) notExecuted(_id) {
        require(approvals[msg.sender][_id] == false);
        require(transfer[_id].hasBeenSent == false);
        approvals[msg.sender][_id] = true;
        transfer[_id].approvals++;
        emit approved(msg.sender, _id);
    }
    
    function executeTransfer(uint _id ) public onlyOwners txExist(_id) notExecuted(_id) {
        require(transfer[_id].approvals >= limit);
        if(transfer[_id].approvals >= limit) {
            transfer[_id].hasBeenSent = true;
            transfer[_id].receiver.transfer(transfer[_id].amount);
        }
        emit executeTransferDone(msg.sender, _id);
    }
    
    function getTransfer() public view returns(Transfer[] memory) {
        return transfer;
    }  
}
Ownable
pragma solidity 0.7.5;

contract Ownable {
    
    address internal owner;
    address[] public owners;

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner of the contract interact!");
        _;
    }
    
    modifier onlyOwners() {
        bool owner = false;
        for(uint i = 0; i < owners.length; i++) {
            if(owners[i] == msg.sender) {
                owner = true;
            }
        }
        require(owner = true);
        _;
    }
    
    constructor() {
        owner = msg.sender;
    }
}

Thank you Carlos for your explanations and confirmation of code !

1 Like

No. When I separate, only Ownable has a constructor.

If MultiSig is Ownable the parent contract is Ownable, right?

Now I got it. I need to set the constructor in both contracts. It was missing in the MultiSig contract. Added:

constructor(address[] memory _owners, uint _requiredApprovals) Ownable(_owners, _requiredApprovals) {}

Now it’s working. Thanks.

1 Like

Ok, in your multiSig contract,

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

you forgot to add the mutability state view

And you have 2 variables with the same name in your ownable contract.

    address internal owner;

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

    constructor() {
        owner = msg.sender;
    }

I advice to call the boolean variable to something else, like isOwner.

Overall you almost have it, those are just minor syntax issues. :nerd_face:

Carlos Z

Thanks for the review! I fixed all syntax issues and logic and removed duplicate owner

1 Like

Hey @JosephGarza, hope you are ok.

Nested structs in solidity has been an issue for so long, in my opinion, they should be avoided, its driven to confusion and apparently is not entirely supported in solidity. Although I did not find info for 0.7.5 version.

This are some of the links that i found about nested structs:

Hope this helps

Carlos Z

Please find below my Multi-signature wallet project. Instead of a constructor for the creator settings, I created a function to set these parameters that only the creator can access. The wallet allows for a dynamic number of owners and transfers. Any feedback would be greatly appreciated.

pragma solidity ^0.7.5;
pragma abicoder v2;
import "./Creator.sol";

contract MultisigWallet is Creator{
     
     struct CreatorSettings {
         address[]  ownerAddresses;
         uint numApprovals;
     }
     
     struct Transfer{
         uint amount;
         address payable receiver;
         bool hasBeenSent;
         uint approvals;
         uint id;
     }
     
    CreatorSettings settings;
    
    modifier onlyOwners {
        bool isOwner = false;
        
         for(uint i=0; i < settings.ownerAddresses.length; i++ ){
            if(msg.sender == settings.ownerAddresses[i]){
                isOwner = true;
                break;
            }
            else{
                isOwner = false;
            }
            
        if (isOwner == true){
            _;
            }
        }

    }
     
     Transfer[] transferRequests;
     
     mapping(address => mapping(uint => bool)) public approvals;
     mapping(address => uint) balance;
     event depositDone(uint amount, address indexed depositedTo);
     
    function deposit() public payable returns (uint)  {
        balance[msg.sender] += msg.value;
        emit depositDone(msg.value, msg.sender);
        return balance[msg.sender];
    }
     
     function setCreatorSettings(address[] memory _ownerAddresses, uint _numApprovals) public  onlyCreator {
         
         settings = CreatorSettings({ownerAddresses:_ownerAddresses, numApprovals:_numApprovals});
         
     }
     
     function getCreatorSettings() public view onlyCreator returns(CreatorSettings memory){
         return settings;
     }
     
     function getBalance() public view returns (uint){
        return balance[msg.sender];
    }
    
     
     //Creates transfer function
     
     function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        require(_amount > 0, "Amount must be greater than zero.");
        require(address(this).balance >= _amount, "Insufficient Funds.");
        
        if (transferRequests.length == 0){
            Transfer memory newTransfer  = Transfer({amount:_amount, receiver:_receiver, hasBeenSent:false, approvals:0, id:0});
            approvals[msg.sender][0] = false;
            transferRequests.push(newTransfer);
        } else{
            Transfer memory newTransfer  = Transfer({amount:_amount, receiver:_receiver, hasBeenSent:false, approvals:0, id:transferRequests.length});
            transferRequests.push(newTransfer);
            approvals[msg.sender][transferRequests.length] = false;
        }
        
     }
     
     function getTransaction(uint _id) public view returns (Transfer memory){
         return transferRequests[_id];
     }
     
     function approveTransaction(uint _id) public onlyOwners(){
         
       if(approvals[msg.sender][_id]  == false){
           transferRequests[_id].approvals += 1;
           approvals[msg.sender][_id] = true;
           
       } 
       
      if (transferRequests[_id].approvals == settings.numApprovals){
         
        require(balance[msg.sender] >=  transferRequests[_id].amount, "Insufficient Balance");
        require(msg.sender !=  transferRequests[_id].receiver, "Don't transfer money to yourself");
        
        uint previousSenderBalance = balance[msg.sender];
        _transfer(msg.sender, transferRequests[_id].receiver, transferRequests[_id].amount);
        assert(balance[msg.sender] == previousSenderBalance - transferRequests[_id].amount);
        transferRequests[_id].hasBeenSent = true;
      }
     }
    
     function _transfer(address from, address to, uint amount) private {
        balance[from] -= amount;
        balance[to] += amount;
    }
}
1 Like

Hey @Nicholas_Blom. hope you are well.

Would you also share the Creator contract? so i can run your project properly :nerd_face:

Carlos Z

Hey @thecil, no problem.

contract Creator {
    
    address public creator;
    
    modifier onlyCreator {
         require(msg.sender == creator);
         _;
    }
    
    constructor(){
        creator = msg.sender;
    }
}
1 Like

Hey there fellas, so I decided to give it a try without help, and soon after got stuck so had to watch a bit more than half of the assistance video. I’m sure that this is definitely not the most efficient way to do it, but was the best I could come up with. Anyhow, I’ll leave you guys my code with its corresponding explanation down below. I really look forward to your feedback.

pragma solidity 0.7.5;

contract Ownable{
    
    address [] owners;
    
    constructor(){
        owners.push(msg.sender);
        owners.push(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        owners.push(0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db);
    }
    
    
    modifier onlyOwners{
        require(msg.sender == owners[0] || msg.sender == owners[1] || msg.sender == owners[2], "You're not an owner");
        _;
    }
    
}

I made a few changes to the ownable contract to ensure that one wallet doesn’t have control over the other owner wallets. The owner addresses are pushed onto the owners array as soon as the contract is created and created a modifier that specifies that only the owners can perform certain functions.

pragma solidity 0.7.5;
pragma abicoder v2;

import "./ownable.sol";

contract MultiSigWallet is Ownable {
    
    constructor(){
        approvalsRequired = 2;
    }
    
    //DECLARATIONS
    uint approvalsRequired;
    mapping (address => mapping(uint => bool)) approves;
   
    
    struct Transaction{
        address sender;
        address payable receiver;
        uint amount;
        uint txID;
        uint approvals;
        string state;
    }
    
    Transaction[] transactions;
    
    //EVENTS DECLARATIONS
    event depositReceived(uint amount, address sender);
    event transactionApproved(address approver, Transaction transaction);
    event transactionCompleted(uint amount, address sender, address receiver);
    
   //FUNCTIONS
    function deposit() public payable{                         
                emit depositReceived(msg.value, msg.sender);
}
    
    
    function createTransaction(address payable receiver, uint amount) public onlyOwners{
        transactions.push(Transaction(msg.sender, receiver, amount, transactions.length, 0, "Pending"));
    }
    
    function approveDisapproveTransaction(uint txID) public onlyOwners{
        approves[msg.sender][txID] = !approves[msg.sender][txID];
        if(approves[msg.sender][txID] == true) transactions[txID].approvals ++;
        else transactions[txID].approvals --;
                
                emit transactionApproved(msg.sender, transactions[txID]);
    }
    
    
    function transfer(uint txID) public onlyOwners{
        
                require(transactions[txID].approvals >= approvalsRequired,"Not enough approvals");
                require(address(this).balance >= transactions[txID].amount, "Not enough funds");
        
        uint walletOriginalBalance = address(this).balance;
        
        transactions[txID].state = "Confirmed";
        transactions[txID].receiver.transfer(transactions[txID].amount);
                
                assert(address(this).balance == walletOriginalBalance - transactions[txID].amount);
                emit transactionCompleted(transactions[txID].amount, transactions[txID].sender, transactions[txID].receiver);
    }
    
    function getTransaction(uint txID) public view returns(Transaction memory){
        return transactions[txID];
    } 

    
    function getWalletBalance() public view returns(uint){
        return address(this).balance;
    }
    
    function getNumberOfApprovals(uint txID) public view returns(uint){
        return transactions[txID].approvals;
    }
    
}

And here is the main multisig wallet contract. First, the minimum amount of approvals required is saved when the contract is created, then the needed data types and events are declared. The function deposit() allows anyone to deposit, the function createTransaction() takes various parameters that will be saved in the array of structs transactions[], the function approveDisapproveTransaction checks whether a transaction has been approved or not by toggling between true or false and adding or substracting approval votes to the approvals property of the Transaction struc, this allows each owner to be capable to vote only once and also provides with an opportunity to remove your approval if necessary. The transfer function checks that amount of approvals matches the minimum amount required and if there is enough funds in the wallet it will perform the transfer and update the state of the transaction to confirmed. And to finalize there are a few other functions that helps us to keep tract of the amount of approvals per transaction, the wallet balance, and the transaction that have been created.

I’ve also implemented event logs, to better keep track of every action and error handling with require() and asser() functions.

I’ve tested this with a different inputs and situations and it all seems to work alright. I’m really looking forward to read your feedback, now I’ll start reading some of the community member’s solutions and see how Filip does it.

CHEERS!!! :mechanical_arm: :mechanical_arm: :mechanical_arm:

1 Like

So at this point of the course, I couldn’t do much more than slightly change an implementation I found online at:
https://solidity-by-example.org/app/multi-sig-wallet/

. Although I tried to do it on my own, my first solution was way below the forum standards. so here is what I ā€œborrowedā€ :slight_smile: …
At least I got to understand all the code…

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

contract Wallet {
    
    address[] public owners;
   
    uint public numComfirmationsRequired = 2;
    
     mapping(address => bool) public isOwner;
    
    struct Transaction{
        address to;
        uint value;
        bool executed;
        uint numConfirmations;
    }
  
    mapping(uint =>mapping(address => bool)) public isConfirmed;
    
    Transaction[] transactions;
    
    
  
    
    constructor(address[] memory _owners){
        require(_owners.length == 3, "3 owners required");
        
        for(uint i = 0; i> _owners.length;i++){
            address owner = _owners[i];
            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "Owner not unique");
            
            isOwner[owner] = true;
            owners.push(owner);
        }
    }
    
   modifier onlyOwner(){
         require(isOwner[msg.sender], "It is required to be an owner");
         _;
    }
    
    modifier transactionExists(uint _txID){
        require(_txID < transactions.length, "Transaction dos not exit");
        _;
    }
    
    modifier notExecuted(uint _txID){
        require(!transactions[_txID].executed, "Transaction already executed");
        _;
    }
    
    modifier notConfirmed(uint _txIndex){
        require(!isConfirmed[_txIndex][msg.sender], "Transaction already confirmed");
        _;
    }
    
    function submitTransaction(address _to, uint _value) public onlyOwner{
       transactions.push(Transaction({to:_to, value:_value, executed: false, numConfirmations:0}) );
    }
    
    function confirmTransaction(uint _txID) public onlyOwner transactionExists(_txID)  notExecuted(_txID) notConfirmed(_txID) {
        Transaction storage transaction = transactions[_txID];
        transaction.numConfirmations +=1;
        isConfirmed[_txID][msg.sender] =true;
    }
    
    function executeTransaction(uint _txID) public onlyOwner transactionExists(_txID) notExecuted(_txID){
        Transaction storage transaction = transactions[_txID];
        require(transaction.numConfirmations >= numComfirmationsRequired, "Cannot execute transaction");
        
        transaction.executed = true;
        (bool sent, ) = transaction.to.call{value: transaction.value}("");
        require(sent, "Failed to send Ether");
    }
    
    function revokeConfirmation(uint _txID) public onlyOwner transactionExists(_txID) notExecuted(_txID){
        Transaction storage transaction = transactions[_txID];
        require(isConfirmed[_txID][msg.sender], "transaction no confirmed");
        
        transaction.numConfirmations -= 1;
        isConfirmed[_txID][msg.sender]=false;
    }
    
    function getOwners() public view returns (address[] memory){
        return owners;
    }
    
    function getTransactionCount() public view returns (uint){
        return transactions.length;
    }
    
    function getTransaction(uint _txID) public view returns (address to, uint value, bool executed, uint numConfirmations){
        Transaction storage transaction = transactions[_txID];
        
        return (transaction.to, transaction.value, transaction.executed, transaction.numConfirmations);
    }
    
}
1 Like

My solution:

pragma solidity 0.8.1;

contract MultiSigWallet{
    
    address owner1;
    address owner2;
    address owner3;
    address requestCreator;
    uint approvalLimit;
    bool pendingRequest = false;
    Request newRequest;
    
    modifier onlyOwner{
        require(msg.sender == owner1 || msg.sender == owner2 || msg.sender == owner3);
        _;
    }
    
    struct Request{
        address destination;
        uint amount;
        uint rejects;
    }
    
    constructor(address _owner1, address _owner2, address _owner3, uint _approvalLimit)
    {
         owner1 = _owner1;
         owner2 = _owner2;
         owner3 = _owner3;
         approvalLimit = _approvalLimit;
    }
    
    function addBalance() public payable returns(uint){
        return address(this).balance;
    }
    
    function createTransferRequest(address _destination, uint _amount) public onlyOwner{
        if(!pendingRequest && address(this).balance >= _amount){
            newRequest = Request(_destination, _amount, 0);
            pendingRequest = true;
            requestCreator = msg.sender;
        }
    }
    
    function approve(bool _sign) public onlyOwner{
        if(pendingRequest && msg.sender != requestCreator){
            if(_sign){
                payable(newRequest.destination).transfer(newRequest.amount);
                pendingRequest = false;
            }else{
                newRequest.rejects++;
                if(newRequest.rejects == approvalLimit){
                    pendingRequest = false;
                }
            }
            
        }
    }
    
}
1 Like

Nice solucion @Nicholas_Blom. :muscle:

So your contract does not have a constructor to initialize, instead you use setCreatorSettings to add the future participants.

So i use this array has participants, and _numApprovalsof 2

     ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", 
     "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
     "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"]

Then, i deposited 3 ethers with the 3rd account of the array, all good by checking the getBalance.

Created a transaction with createTransfer and was not able to checked with getTransaction, this could be a solution for that:

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

Which will return all the transactions.

I’m facing an issue where, even approveTransaction with id 0, it does execute, but it does not change the boolean value to true, might want to check it out, when i triggered the approvals, the returned value was always false. :face_with_monocle:

Another suggestion would be that in case that the transaction is approved, the transfer of funds is only inside the contract (mapping balance, from one address to another, but only inside the mapping), so there is no way to withdraw the funds from the contract, i know there is one lesson in the course which explain how to :nerd_face:

Overall you almost finish, just some few improvements and you will made it!

Carlos Z

Amazing solution @Ernest_SG :muscle:

After deployment, it already add the proper remix accounts and set the approvalsRequired in the constructor, then I deposited 3 ethers (1st account, contract owner) and getWalletBalance properly :nerd_face:

For createTransaction I triggered with 2nd account to the 4th account in remix (non-owner), although the 1st account was the one that deposited the funds, the transaction was created properly (checked with getTransaction)

Then I approved the transaction id 0 with 1st account, and triggered again to check the disapprove function, i was able to toggling twice while checking with getNumberOfApprovals and getTransaction and it increment or decrement for each call. Nice solution here man :muscle:

Then I just approved the transaction with the 3rd account and transfer the funds properly to the 4th account.

Few suggestions is for this:
transactions[txID].state = "Confirmed"; Although is a good solution, keep in mind that solidity is not friendly with string values (gas fees), that way, i know there is a lesson in Ethereum SC Programming 201 that will teach you to use the enum statement, which can have the same effect but in a more efficient way :nerd_face:

Overall you have made an amazing job man! congrats :partying_face:

Carlos Z

1 Like