Project - Multisig Wallet

how do you mean can u elaborate your exact issue. so when your transfer say 1 eth what happens. the recipient doesnt recieve the correct amount or what exactly?

Exactly. the recipient gets less than 1 ETH in that case. Let me modify the code for several users and try again.

1 Like

i took your code into remix. the recipient does get the full amount. i sent this address 1 ether and he now has 101 as expected

Let me try again. I certainly didn’t get that result.

1 Like

cool let me know. i say you may have decieved yourself and when u went to copy the address of the person u were sending to for a new transfer you may have accidently called the create transfer func as the address before switching back to an oweners account which would have enacted a fee on the recipient

I got it working this time (no idea what I did different though). Here is the new code:

pragma solidity 0.7.5;
pragma abicoder v2;

import "../Inheritance/Destroyable.sol";

contract MultiSig is Destroyable {

mapping(address => uint) balance;

address[] private owners;
uint private req_approvals;

struct Transfer {
    uint amount;
    address payable recipient;
    address sender;
    uint approvals;
    bool sent;
}

// Note 1: this will only grow since we want to keep track of all transfers.
// Note 2: we use the array index for the transfer id.
Transfer[] transfers; 

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

event fundsDeposited(uint amount, address indexed depositedTo);

modifier enoughBalance (address sender_, uint amount_) {
    require(balance[sender_]>= amount_, "Not enough funds");
    _; //Run the function
}

modifier isOwner () {
    require (semderIsOwner(), "Only owners may approve");
    _; //Run the function
}

constructor(address[] memory _s, uint _appr){
// We dont worry about double addreses in owners for now. Possible imporvement for later.
    owners=_s;
    req_approvals=_appr;
}

function getTransfer(uint tid_) view public returns (Transfer memory) {
    return transfers[tid_];
}

function semderIsOwner() view public returns (bool){
    // 
    bool result=false;
    for (uint index=0;index<owners.length;index++){
        if (owners[index]==msg.sender) {
            result=true;
            break;
        }
    }
    return result;
}

function approve (uint tid_) isOwner public {
    require (approvals[msg.sender][tid_]==false, "Already approved");
    approvals[msg.sender][tid_]=true;
    transfers[tid_].approvals++;
    if (transfers[tid_].approvals>=req_approvals && transfers[tid_].sent==false) {
    // >= should not be required. Maybe just == since the >= should never happen...
        transfer(transfers[tid_].recipient, transfers[tid_].sender, transfers[tid_].amount);
        transfers[tid_].sent=true;    
    }    
}

function newTransfer (address payable recipient_, uint amount_) isOwner public returns (uint) {
    require(address(this)!= recipient_, "Dont transfer to yourself");
    uint tid; // Transaction id
    Transfer memory nt;
    nt.amount=amount_;
    nt.recipient=recipient_;
    nt.sender=msg.sender;
    nt.approvals=0;
    nt.sent=false;
    tid=transfers.length; // ID is the size of the array before the push
    transfers.push(nt);
    approve (tid); // We assume that the creator of a new transfer also wants to approve it so we do that right away.
    return tid;
}

function deposit() public payable returns(uint) {
    balance[msg.sender]+= msg.value;
    emit fundsDeposited(msg.value, msg.sender);
    return balance[msg.sender];
}

function transfer  (address payable recipient_, address sender_, uint amount_) enoughBalance(sender_, amount_) private {    
   uint previousSenderBalance=balance[sender_];
    balance[sender_]-=amount_;
    balance[recipient_]+=amount_;

    recipient_.transfer(amount_);
    assert (balance[sender_]==previousSenderBalance-amount_);
}

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

}
1 Like

happy days well done with this project. i like your solution

1 Like

Hi together, see below my solution :slight_smile:

The contract needs to be deployed with at least 3 Admin-addresses who can request & approve transfers. Also it needs to be deployed with requested_approvals >= 2.

Here an example of valid parameters for deployment:

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

However, the contract is flexible enough to take more than 3 admin-addresses in deployment and more than 2 requested_approvals. So without further delay here the contract:

pragma solidity 0.7.5;

/**

– Anyone is able to deposit ether into the smart contract

– The contract creator OWNER is REQUIRED to input (1): At least 3 ADMIN addresses and (2):  the numbers of approvals required for a transfer WITH MIN 2.

– Anyone of the ADMINs is able to create a transfer request. The creator of the transfer request will specify what amount and to what address the transfer will be made.

– ADMINs are able to approve transfer requests.

– When a transfer request has the required approvals, the transfer will be sent to recipient.

**/

contract MultiSig {

    uint private required_approvals;

    uint private transaction_id = 0;

    mapping(address => uint) balance;

    mapping(address => bool) private admin;

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

    struct TransactionRequest {

        uint id;

        address requested_from;

        address payable recipient;

        uint amount;

        uint nApprovals;

    }

    TransactionRequest[] transactionrequests;

    modifier onlyAdmins() {

        require(admin[msg.sender]);

        _; //run the function

    }

    event depositDone(uint amount, address depositedTo);

    constructor(address[] memory adminNames,uint _required_approvals){

        require(_required_approvals >= 2, "A minimum of 2 approvals is required");

        require(adminNames.length >= 3, "A minimum of 3 Admin-addresses is required");

        require(adminNames.length > _required_approvals, "Number of Admins must be greater than number of approvals");

        //set addresses in adminNames as owner

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

            admin[adminNames[i]] = true;

        }

        //set required approvals

        required_approvals = _required_approvals;

    }

    function deposit() public payable returns (uint) {

        balance[msg.sender] += msg.value;

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

        return balance[msg.sender];

    }

    function requestTransfer(address payable _recipient, uint _amount) public onlyAdmins{

        TransactionRequest memory newRequest = TransactionRequest(transaction_id,msg.sender,_recipient,_amount,0);

        transaction_id++;

        transactionrequests.push(newRequest);

    }

    function viewTransfer(uint _transaction_id) public view returns (uint, address, address, uint, uint){

        return(

            transactionrequests[_transaction_id].id,

            transactionrequests[_transaction_id].requested_from,

            transactionrequests[_transaction_id].recipient,

            transactionrequests[_transaction_id].amount,

            transactionrequests[_transaction_id].nApprovals

        );

    }

    function approveTransfer(uint _transaction_id) public onlyAdmins returns(uint){

        require(approvalAddresses[_transaction_id][msg.sender] == false, "You already approved this transfer request");

        require(transactionrequests[_transaction_id].requested_from != msg.sender, "You can't approve your own requests");

        require(transactionrequests[_transaction_id].nApprovals < required_approvals, "The request has already been approved");

        transactionrequests[_transaction_id].nApprovals++; //increase approvals

        approvalAddresses[_transaction_id][msg.sender] = true; //set approval by msg.sendet to true

        if( transactionrequests[_transaction_id].nApprovals == required_approvals) { //check if required approvals are reached

            transactionrequests[_transaction_id].recipient.transfer(transactionrequests[_transaction_id].amount); //transfer to recipient

        }

        return transactionrequests[_transaction_id].nApprovals;

    }
   
}
1 Like

Consensus Layer

pragma solidity 0.7.5;

contract consensus {

uint consensusNumber;
uint participantNumber;
mapping(address => bool) participation;


function numberNeeded(address _added) internal {
        if (participation[_added] == false) {
            participation[msg.sender] = true;
            participantNumber ++;
        }
        uint i = 0;
        while (i < participantNumber){
            uint _numerator  = i * 10 ** (4);
            uint _quotient =  ((_numerator / participantNumber) + 5) / 10;
            if (_quotient >= 667) {
                consensusNumber = i;
                break;
            } else {
                i++;
            }

}

}

}

Deposits Layer

pragma solidity 0.7.5;
import “./consensus.sol”;

contract deposits is consensus {

event depositedMoney(address indexed from, uint amount);

struct Participant {
    address fromParticipant;
    uint amountParticipant;
}

address[] participants;

function depositMoney() public payable {
    participants.push(msg.sender);
    emit depositedMoney(msg.sender,msg.value);
    numberNeeded(msg.sender);
}

function viewParticipantAddresses() public view returns (address[] memory) {
    address[] memory people = participants;
    return people;
}

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

function viewConsensusNeeded() public view returns (uint) {
    return consensusNumber;
}

function viewParticipantNumber() public view returns (uint) {
    return participantNumber;
}

}

Transfer Layer

pragma solidity 0.7.5;
import “./deposits.sol”;

contract transfer is deposits {

modifier onlyParticipants {
    require(participation[msg.sender] == true); 
    _;
}

struct Transfer {
    uint amount;
    address toAddress;
    uint voteCount;
}

Transfer[] transferArray;
address[] addresses;
mapping(address => uint) voted;


function initiateTransfer(uint _amount, address _to) onlyParticipants public {
    require (_amount <= address(this).balance);
    transferArray.push(Transfer(_amount, _to, 1));
    voted[msg.sender] += 1;
    addresses.push(msg.sender);
}

function votes(uint decision) onlyParticipants public payable {
    require(voted[msg.sender] != 1);
    if (decision == 1){
        transferArray[0].voteCount ++;
        voted[msg.sender] += 1;
        addresses.push(msg.sender);
    } else  {
        voted[msg.sender] += 1;
        addresses.push(msg.sender);
    } 
    if(transferArray[0].voteCount >= consensusNumber && transferArray[0].amount <= address(this).balance) {
        payable(transferArray[0].toAddress).transfer(address(this).balance);
            delete transferArray;
            for (uint i = 0; i < addresses.length; i++){
                voted[addresses[i]] = 0;
            }
            if (address(this).balance == 0) {
                participantNumber = 0;
                consensusNumber = 0;
                for (uint i = 0; i < participants.length; i++){
                    participation[participants[i]] = false;
                }
            }
        
        
    }
}

function checkBalance(address thisAddress) public view returns (uint) {
    return thisAddress.balance;
}

}

I didn’t follow Filip’s guidance on this project as I wanted to give it a shot myself first. I saw his implementation videos after to see if I missed anything though. Thus, I ended up with a solution that doesn’t rely on a map within a map data structure. In my testing, it works well so I’d love to hear feedback on my solution if anyone has the opportunity to go through it :slight_smile:

Here it is in Github, if you prefer reading the code there: https://github.com/0xkgoel

Ownable.sol

pragma solidity 0.7.5;

contract Ownable {
	mapping(address => bool) owners;
	uint minApprovals;
	
	modifier onlyOwners {
		require(owners[msg.sender] == true);
		_;
	}
	
	constructor(address[] memory _owners, uint _minApprovals) {
		for(uint i = 0; i < _owners.length; i++) {
			owners[_owners[i]] = true;
		}
		minApprovals = _minApprovals;
	}
}

MultiSigWallet.sol

pragma solidity 0.7.5;
pragma abicoder v2;

import "./Ownable.sol";

contract MultiSigWallet is Ownable {
    struct Trx {
		uint id;
		address from;
		address payable to;
		uint amount;
        bool completed; //track if the transfer has already gone through to prevent further unnecessary approvals
	}

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

	mapping(uint => address[]) outstandingTransfers; //Mapping of Transaction ID to List of Approvers
	Trx[] trxs; //Array to hold our transaction/transfer objects
	uint idCounter = 0;

	event transferRequested(address indexed from, address indexed to, uint amount);
    event txCreated(Trx trx);
	event transferApproved(address indexed approver, uint indexed txId);
    event transferCompleted(uint indexed txId, address to, uint amount);
    event approvalGranted(address indexed by, uint indexed forTx);

	function _getId() private returns(uint) {
		return idCounter++;
	}
	
	function deposit() external payable {}

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

	function requestTransfer(uint _amount, address payable _to) public onlyOwners returns(Trx memory) {
        require(_amount <= address(this).balance, "Insufficient Balance to Request Transfer");
		uint txId = _getId();
		Trx memory trx = Trx(txId, msg.sender, _to, _amount, false);
        emit txCreated(trx);

		outstandingTransfers[txId].push(msg.sender);
		emit transferRequested(msg.sender, _to, _amount);
        
        trxs.push(trx);
        return _initiateTransfer(txId, outstandingTransfers[txId].length);
	}

	function approveTransfer(uint _id) public onlyOwners returns(Trx memory) {
		address[] storage approvals = outstandingTransfers[_id];
		bool previouslyApproved = false;

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

		require(!previouslyApproved, "Transfer previously approved");
		approvals.push(msg.sender);
        emit approvalGranted(msg.sender, _id);
		outstandingTransfers[_id] = approvals;
		
		return _initiateTransfer(_id, approvals.length);
	}

	function _initiateTransfer(uint _id, uint _approvals) private returns(Trx memory) {
		Trx memory trx = trxs[_id];

        if(_approvals >= minApprovals) {
            require(!trx.completed, "Transfer already completed");
            require(trx.amount <= address(this).balance, "Insufficient Balance to Complete Transfer");
			trx.to.transfer(trx.amount);
            emit transferCompleted(_id, trx.to, trx.amount);
            trx.completed = true;
		}

        return trx;
	}
}

When calling the function Add, I get the following error, why ?

The transaction has been reverted to the initial state.
Note: The called function should be payable if you send value and the value you send should be less than your current balance.
Debug the transaction to get more information.

uint[][] public Matrix;

    uint ID_1 = 3 ;
    uint ID_2 = 4;

    function Add()  public{
        Matrix[ID_1][ID_2] = 1;
    }
1 Like

ok so there is a few reasons why this wont work. its an issue with your concept of data structures. your trying to define a 2D array or a 2D matrix. so a 2D matrix has a (x) and (y) direction if were thinking in terms of space so the out put of the martic will always have the form

[ xDim ]
[ yDim ]

we can change the number of X dimentions an Y dimesnions to change the shape of our array. for example a 2 x 3 matrix will look like

[ 0,0 ][ 0,1 ]
[1,0 ][ 1, 1 ]
[ 2,0 ][ 2,2 ]

so in your code you are trying to set your matric to a interger value. arrays are not of interger type. so you cannot just set it equal to one. the error is because this is a type mismatch. if you want to change evry element (or some) in a multi-dimensional array the only way to do so is to use a nested for loop.

The other reason its failing, is because in many computer programming languages, if you want to define a multi-dimensional array you must befine its shape on declaration. so your line of code

uint[][] public Matrix'

will throw an error on runtime because you cant create an array like this with undefined dimensions. this is to do with memory management and allocation and you you have experinec wit C or C++ you will know all about this. Now on this note in C you can use methods to create an array of dynamic size but in order to do so you need to allocate the memory for the array on its creation. i wont get into this as its out of the scope of this awnser and incase your unfamiliar with C i dont want to confuse you.

Solidity has some similar fundamentals to the C programming language, but sadly in soldidty you can not allocate memory to a dynamic array on creation so the only way to make one is if you know its dimensions before hand. You could perhaps use somthing like new Array() and pass in the dims, but we will not touch on that.

therefore the first change you need to make is to declare your array with its dimensions define. so for example we can use a 2 x 2 array

uint[2][2] public Matrix;

this will now work and you will not get any errors on at runtime. (note solidity wont catch the undefined shape error on compile time, the error only occurs at runtime, same in C).

now onto the population function. defining the array above like we have does not mean that the array is going to be filled with twos, it only means it has a dimenion of 2 x 2, so our array at this point is still empty and we need to populate it. so we will use your Add function and modify it. to re-iterate we can only populate 2D arrays with nested for loops because we need to individually traverse each elemnt in the array and update it. so the correct implemetnation of the Add function is

function Add()  public returns(bool success) {
      
        for(uint i = 0; i < 2; i++) {
            for(uint j = 0; j < 2; j++) {
                Matrix[i][j] = 1;
            }
        }

        return success;
    }

so here we loop over the x and y dimentions and for each x,y co-ordinate we can set the value. for simplicity i just set the value to one. you could however use conditional statements to populate the array but we wont do that here

now we can make a function getMatrix to return the array. it looks as follows

 function getMatrix() public view returns(uint[2][2] memory) {

        return Matrix;
    }

this function will return uint[][] => 1, 1, 1, 1. So that how you can deal with multidimensional arrays in solididyt. but there is one thing. this is crazy crazy costly with gas because were using nested for loops. so please never ever ever use a 2d array data structure in solididty. a better way to do it is with a mapping. which is essentially an array. ive written a modified and optimized way of your the array contract above which used a mapping instead of an array

ontract Wallet2 {
    
    mapping(uint => mapping(uint => uint)) Matrix;

    function Add()  public {
      
        for(uint i = 0; i < 2; i++) {
            for(uint j = 0; j < 2; j++) {
                Matrix[i][j] = 1;
            }
        }
    }

    function getMatrix(uint a, uint b) public view returns(uint) {

        return Matrix[a][b];
    }
    
}

the only thing here is we cannot return the whole array. we can only return specific co-ordinates.

Let me know if you have any questions

1 Like
pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    address[] public owners;
    uint limit;
    mapping(address => uint) balance;
    address senderAddress;

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

    Transfer[] transferRequests;

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

    // mapping[address][transferID] => true/false

    //Should only allow people in the owners list to continue the execution.
    modifier onlyOwners(){
        bool isOwnerAvailable = false;
        for (uint i; i < owners.length; i++) {
            if (owners[i] == msg.sender) {
                isOwnerAvailable = true;
            }
        }
        require(isOwnerAvailable == true);
        _;
    }

    //Should initialize the owners list and the limit 
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;

        // for tracking purpose only, I always use the first address to keep track
        senderAddress = _owners[0];
    }
    
    //Empty function
    function deposit() public payable {
        balance[msg.sender] += msg.value;
    }
    
    //Create an instance of the Transfer struct and add it to the transferRequests array
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        uint id = transferRequests.length + 1;
        transferRequests.push(Transfer(_amount, _receiver, 0, false, id));
    }
    
    //Set your approval for one of the transfer requests.
    //Need to update the Transfer object.
    //Need to update the mapping to record the approval for the msg.sender.
    //When the amount of approvals for a transfer has reached the limit, this function should send the transfer to the recipient.
    //An owner should not be able to vote twice.
    //An owner should not be able to vote on a tranfer request that has already been sent.
    function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id-1] == false);
        require(transferRequests[_id-1].hasBeenSent == false);

        transferRequests[_id-1].approvals += 1;
        approvals[msg.sender][_id] = true;
        if (transferRequests[_id-1].approvals >= limit) {
            transferRequests[_id-1].hasBeenSent = true;
            transferRequests[_id-1].receiver.transfer(transferRequests[_id-1].amount);

            // for tracking purpose
            balance[senderAddress] -= transferRequests[_id-1].amount;
        }
    }
    
    //Should return all transfer requests
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    }

    function getBalance() public view returns (uint) {
        return balance[senderAddress];
    }
}

Wow I learned a lot here thanks.
Yes I hadn’t taken into account at that stage the gas cost for the operations, but it makes sense now.
I realize why that Double Mapping is needed now.

2 Likes

great. glad i could help. and no worries.

1 Like

My final version:

I’d love to have your opinion on it as I really did 100% myself without the help videos.
The differences I noticed comparing the solution from the video are the following :

  • My Double Mapping is inversed : First you provide the Transaction ID, then you can check which owners have approved it yet
  • My onlyOwner modifier structure is very different. Is it lighter/heavier to run ?
  • other differences too
//SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;
pragma abicoder v2;


contract MultiSigWallet {

    uint ApprovalRequired;
    address[] ArrayOwners ;
    mapping(address => uint) OwnerID ;

    constructor (address[] memory _ArrayOwners, uint _ApprovalRequired) {
        ApprovalRequired = _ApprovalRequired;
        ArrayOwners= _ArrayOwners;
        for (uint i=0;i<ArrayOwners.length;i++) {
            OwnerID[ArrayOwners[i]] = i+1 ;
        } 
    }

    event TransferRequested (uint TransferID,address RequestedBy, address TransferTo, uint amount);
    event TransferDone (uint AmountTransferred, address indexed SentTo) ;
    event DepositDone (address indexed Depositer, uint AmountDeposited);


    struct TransferRequest {
        uint TransferID ;
        address initiator ;
        address TransferTo;
        uint Amount;
        bool TransferredSuccessfully;
    }

    TransferRequest[] public ListTransferRequests ; // public so that anyone can look for a transferID and see the request

      modifier OnlyOwner {
         require (IsOwner() == true);
         _;
    }

    function IsOwner() public view returns (bool){ 
          for (uint i=0 ; i < ArrayOwners.length ; i++) {
              if (ArrayOwners[i] == msg.sender) { 
                  return true ; 
                  } 
          }
          return false;
    }

    mapping (uint => mapping (address=> bool)) ApprovalMapping ; // we provide the Transaction ID first then the owner address


    function CreateTransferRequest (uint amountToTransfer, address recipient) public OnlyOwner {
        require( balance[recipient] >= amountToTransfer, "This account doesn't have enough balance for that transfer" ); // we check it before pushing the list AND after getting the approval (in the transfer function) as a double check
        ListTransferRequests.push(TransferRequest(ListTransferRequests.length,msg.sender,recipient,amountToTransfer,false));
         ApprovalMapping[ListTransferRequests.length-1][msg.sender] = true; 
         emit TransferRequested(ListTransferRequests.length-1,msg.sender,recipient,amountToTransfer);
    }

    function AcceptTransferRequest (uint TransferID)  public OnlyOwner {
        ApprovalMapping[TransferID][msg.sender] = true;
        if (CheckApprovals(TransferID) == true && ListTransferRequests[TransferID].TransferredSuccessfully== false ) {
           TransferTo(ListTransferRequests[TransferID].Amount,ListTransferRequests[TransferID].TransferTo,TransferID ); //make transfer
        }
    }

    function CheckApprovals (uint TransferID) view public returns (bool){
        uint Approvals = 0 ;
        for (uint i=0 ; i < ArrayOwners.length ; i++) {
            if (ApprovalMapping[TransferID][ArrayOwners[i]] == true) {Approvals += 1;}
        }
        if (Approvals >= ApprovalRequired) {return (true);}
        return (false);
    }

    mapping (address => uint) balance ;

    function MakeADeposit () public payable returns (uint) {
         balance[msg.sender] = balance[msg.sender] +msg.value;
         emit DepositDone (msg.sender, msg.value);
        return balance[msg.sender] ;
    }
    
    function TransferTo (uint amountToTransfer, address recipient, uint TransferID) private returns(uint, address) {
        require( balance[recipient] >= amountToTransfer);
        uint previousBalance = balance[recipient];
        balance[recipient] -= amountToTransfer;
        payable(recipient).transfer(amountToTransfer);
        // dire que c'est sent pour pas qu'on puisse le faire à double
        ListTransferRequests[TransferID].TransferredSuccessfully = true ;
        emit TransferDone (amountToTransfer, recipient) ;
        assert(balance[recipient] == previousBalance - amountToTransfer);
        return (amountToTransfer, recipient) ;
    }

// extra helpful function
    function GetBalance(address BalanceFrom) public view returns (uint) {
        return balance[BalanceFrom] ;
    }

    function GetListOwners() public view returns ( uint, address[] memory){
           return (ArrayOwners.length, ArrayOwners);
    }
    function GetOwnerID(address ToTest) public view returns(uint IfZeroNotOwner) {
        return OwnerID[ToTest];
    }

}
1 Like

I’ve made changes in my previous solution (it was removed to prevent noise) for removing the redundant balance map within MultisigWallet contract and added the possibility to init the source wallet owner within MultisigWallet constructor.

Ownable.sol

// Ownable.sol
pragma solidity 0.8.13;

contract Ownable {
    mapping(address => bool) owners;

    modifier onlyOwners {
        require(owners[msg.sender] == true);
        _;
    }

    event ownerAdded(address newOwnerId);

    function addOwner(address _newOwner) public onlyOwners {
        owners[_newOwner] = true;
        emit ownerAdded(_newOwner);
    }
}

MultisigWallet.sol

// MultisigWallet.sol
pragma solidity 0.8.13;

import "./Ownable.sol";

contract MultisigWallet is Ownable {
    struct Transfer {
        address from;
        address payable to;
        uint amount;
        mapping(address => bool) approvals;
        uint approvalsCounter;
        uint id;
    }
    
    Transfer[] transfers;
    uint approvalLimit;

    event depositDone(address sender, uint amount, uint resultBalance);
    event approvalLimitChanged(uint newApprovalLimit, address changedBy);
    event transferInitialized(address from, address to, uint amount, uint createdTransferId);
    event transferApproved(uint transferId, uint approvalsCount, uint currentApprovalLimit);
    event transferFinished(address from, address to, uint amount);

    constructor(address _sourceOwner) {
        owners[_sourceOwner] = true;
    }

    function deposit() public payable {
        emit depositDone(msg.sender, msg.value, address(this).balance);
    }
    
    function setApprovalLimit(uint _approvalLimit) public onlyOwners {
        approvalLimit = _approvalLimit;
        emit approvalLimitChanged(approvalLimit, msg.sender);
    }

    function initTransfer(address payable _to, uint _amount) public onlyOwners {
        require(address(this).balance >= _amount);

        Transfer storage newTransfer = _createTransfer(_to, _amount);
        emit transferInitialized(msg.sender, _to, _amount, newTransfer.id);
        if (approvalLimit == 0) {
            _finishTransfer(msg.sender, _to, _amount);
        }
    }

    function approveTransfer(uint _id) public onlyOwners {
        Transfer storage selectedTransfer = transfers[_id];
        require(selectedTransfer.approvalsCounter <= approvalLimit, "Required approvals already received.");
        require(selectedTransfer.approvals[msg.sender] != true, "Approve must be unique.");

        selectedTransfer.approvals[msg.sender] = true;
        selectedTransfer.approvalsCounter++;
        emit transferApproved(_id, selectedTransfer.approvalsCounter, approvalLimit);
        if (selectedTransfer.approvalsCounter >= approvalLimit) {
            _finishTransfer(selectedTransfer.from, selectedTransfer.to, selectedTransfer.amount);
        }
    }

    function _finishTransfer(address _from, address payable _to, uint _amount) private {
        require(address(this).balance >= _amount);
        // The transfer solution used because of: https://ethereum.stackexchange.com/a/19343
        (bool success, ) = _to.call{value:_amount}("");
        assert(success);

        emit transferFinished(_from, _to, _amount);     
    }

    // This function is requred because the Transfer struct contains mapping
    // and there are no possibility to construct it with empty mapping in a Transfer(args...) way
    function _createTransfer(address payable _to, uint _amount) private returns (Transfer storage) {
        Transfer storage newTransfer = transfers.push();
    
        newTransfer.from = msg.sender;
        newTransfer.to = _to;
        newTransfer.amount = _amount;
        newTransfer.id = transfers.length - 1;

        return newTransfer;
    }
}
1 Like
// SPDX-License-Identifier: MIT

pragma solidity 0.8.13;

// A multi sig wallet smart contract
// A use case might be a charity where anyone can send in funds, anyone can request funds and
// the charity trustees(referenced as owners in the smart contract) can approve or reject
// the request for funds
// This smart contract is configured for three(3) trustess with two(2) yay or nay votes
// required by the trustees to either approve or reject the request for funds
// A trustee can change their vote before the request for funds is either approved or rejected
// Once a request for funds is either approved or rejected it is marked as closed and
// voting will be closed for that particular 'transfer of funds' request

contract MultiSigWallet {
    struct TransferRequests {
        string transferReason;
        address transferTo;
        uint transferAmount;
        bool transferApproved;
        bool transferRejected;
        bool transferRequestClosed;
        mapping(uint => bytes1) vote;
    }

    mapping(uint => TransferRequests) public transferRequests;

    address[3] public owners;
    uint public approvalsRequired;
    uint private id;

    event DepositReceived(address indexed _depositor, uint _depositAmount);
    event TransferRequestReceived(
        string indexed _transferReason,
        address indexed _transferTo,
        uint _transferAmount,
        uint _assignedID
    );
    event Transferred(
        string indexed _transferReason,
        address indexed _transferTo,
        uint _transferAmount
    );
    event TransferRejected(
        string indexed _transferReason,
        address indexed _transferTo,
        uint _transferAmount
    );

    constructor(address[3] memory _addresses, uint _approvalsRequired) {
        owners = _addresses;
        approvalsRequired = _approvalsRequired;
    }

    function deposit() external payable {
        emit DepositReceived(msg.sender, msg.value);
    }

    function checkContractBalance() external view returns (uint _balance) {
        _balance = address(this).balance;
    }

    function createTransferRequest(
        string memory _transferReason,
        address _transferTo,
        uint _transferAmount
    ) public {
        TransferRequests storage newTransferRequest = transferRequests[id];
        uint _assignedID = id;
        id++;

        newTransferRequest.transferReason = _transferReason;
        newTransferRequest.transferTo = _transferTo;
        newTransferRequest.transferAmount = _transferAmount;

        emit TransferRequestReceived(
            _transferReason,
            _transferTo,
            _transferAmount,
            _assignedID
        );
    }

    function vote(uint _id, bool approve) external {
        uint voterIndex;
        bool _isInOwnerList;
        uint yesVotes;
        uint noVotes;

        for (uint i = 0; i < 3; i++) {
            if (msg.sender == owners[i]) {
                _isInOwnerList = true;
                voterIndex = i;
            }
        }

        require(_isInOwnerList, "Only owners can perform this action");
        require(
            keccak256(bytes(transferRequests[_id].transferReason)) !=
                keccak256(bytes("")),
            "The transfer request was not found for the provided ID"
        );
        require(
            transferRequests[_id].transferRequestClosed != true,
            "The transfer request is closed"
        );

        if (approve) {
            transferRequests[_id].vote[voterIndex] = "Y";
        } else {
            transferRequests[_id].vote[voterIndex] = "N";
        }

        for (uint i = 0; i < 3; i++) {
            if (transferRequests[_id].vote[i] == "Y") {
                yesVotes++;
            }
            if (transferRequests[_id].vote[i] == "N") {
                noVotes++;
            }
        }

        if (yesVotes == approvalsRequired) {
            transferRequests[_id].transferApproved = true;
            transferRequests[_id].transferRequestClosed = true;
            (bool success, ) = transferRequests[_id].transferTo.call{
                value: transferRequests[_id].transferAmount
            }("");
            require(success, "Transfer failed");
            emit Transferred(
                transferRequests[_id].transferReason,
                transferRequests[_id].transferTo,
                transferRequests[_id].transferAmount
            );
        } else if ((noVotes == approvalsRequired)) {
            transferRequests[_id].transferRejected = true;
            transferRequests[_id].transferRequestClosed = true;
            emit TransferRejected(
                transferRequests[_id].transferReason,
                transferRequests[_id].transferTo,
                transferRequests[_id].transferAmount
            );
        }
    }
}
1 Like

Help me to fix the error below!

My code was successfully compiled, but I got an error below during the deployment.

Error: expected array value (argument=null, value="", code=INVALID_ARGUMENT, version=abi/5.5.0)

This is my wallet code. I created a list of request (each request stores the amount, recipient, and list of voters). If the owner signs, the address would be added to the list of voters. The length of list means the number of approvals. Transaction will be made once the approval number is the same as the initial threshold.

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    // minimum number of approvals to make a transaction
    uint threshold;

    // return true if the owner address is saved as a key in the dict
    mapping(address => bool) isOwnerDict;

    struct Request {
        address payable recipient;
        address[] voters;
        uint amount;
    }
    Request[] requestLog;

    modifier onlyOwners() {
        require(isOwnerDict[msg.sender] == true);
        _;
    }

    modifier stopSecondVote(uint _id) {
        bool isSecondVote = false;
        for (uint i=0; i<requestLog[_id].voters.length; i++) {
            if (requestLog[_id].voters[i] == msg.sender) {
                isSecondVote = true;
            }
        }
        require(isSecondVote = false);
        _;
    }

    // setup the initial values (owners and minimum number of approvals)
    constructor(address[] memory _ownerList, uint _threshold) {
        threshold = _threshold;
        for (uint i=0; i<_ownerList.length; i++) {
            isOwnerDict[_ownerList[i]] = true;
        }
    }

    // create a request 
    function createRequest(address payable _recipient, uint _amount) public onlyOwners {
        address[] memory emptyVoterList;
        requestLog.push(
            Request(_recipient, emptyVoterList, _amount)
        );
    }

    // approve a request and transfer the amount
    function approve(uint _id) public onlyOwners stopSecondVote(_id) {
        addVoter(_id);
        transfer(_id);
    }

    // add the owner address in the voter list
    function addVoter(uint _id) private {
        requestLog[_id].voters.push(msg.sender);
    }

    // transfer the requested asset once 
    function transfer(uint _id) private {
        require(requestLog[_id].voters.length == threshold);
        requestLog[_id].recipient.transfer(requestLog[_id].amount);
    }
}
1 Like

where does the error throw. when calling what fuctiom? im going to assume ots from your constrictot. you need to pass in an array of addresses and a threshold value before u deploy it
in remix you will see an option to input params beside the deploy tab. this is where yoy do it. arg1 should be an array of owners and arg2 should be the threshold number

1 Like