Project - Multisig Wallet

I pasted my first attempt below.

Mappings brought me a few headaches because they cannot be iterated and they don’t have a length, therefore I eventually reverted to dynamic arrays in places. I guess it is fine since the size of the arrays is small.

The different owner operations are separated (request a transfer, approve a transfer, execute a transfer). I was tempted to cascade some of them but eventually gave up because it increases complexity. For instance

  • requesting a transfer does not auto-approve;
  • approving a transfer does not auto-execute if all approvals are present.

I’m curious to look at the solution now! :yum:

I’m also curious to find out how to write unit-tests in Solidity.

Matt

pragma solidity 0.7.5;
pragma abicoder v2;


contract MultisigWallet{
    
    mapping( address => bool) ownersMapping;
    address[] owners;
    
    uint minApprovals;
    
    struct Request{
        address payable recipient;
        uint amount;
        address[] approvals;
        bool transferDone;
    }

    Request[] requests; 


    modifier onlyOwner{
        require(ownersMapping[msg.sender] == true, "Can be called only by one of the owners");
        _;
    }
    
    modifier validRequestId(uint requestId){
        require(requestId < requests.length, "Invalid request id");
        _;
    }

    constructor(address[] memory _owners, uint _minApprovals){
        
        require(_owners.length <= 5, "No more than 5 owners");
        require(_owners.length >= 1, "Specify at least 1 owner");
        require(_minApprovals <= _owners.length, "There cannot be more approvers than owners");
        require(_minApprovals >= 1, "There should be at least 1 approver");
      
        for(uint i=0; i < _owners.length; ++i){
            ownersMapping[_owners[i]] = true;  
        }
        owners = _owners;
        
        minApprovals = _minApprovals;
    }

    // Add funds to the wallet
    function deposit() external payable returns (uint balance){
        
        return address(this).balance;
    }

    //Log a transfer request and return a request id to be used for approval and execution of the transfer
    function requestTransfer(address payable recipient, uint amount) external onlyOwner returns (uint requestId){
        require(address(this).balance >= amount, "Not enough balance!");

        Request storage newRequest = requests.push();

        newRequest.recipient = recipient;
        newRequest.amount = amount;
        newRequest.transferDone = false;
        

        // Return request id to be used to approve
        return requests.length-1;
    }
    
    // Approve transfer request by request id. Returns number of missing approvals
    function approveTransfer(uint requestId) external onlyOwner validRequestId(requestId) returns (uint missingApprovals) {

        Request storage requestToApprove = requests[requestId];
        
        require(requestToApprove.transferDone == false, "This transfer request was already executed");
        require(!hasApproved(requestId, msg.sender), "You already approved this request");

        requestToApprove.approvals.push(msg.sender);
        
        assert(requestToApprove.approvals.length <= owners.length);
        
        return (minApprovals-requestToApprove.approvals.length);
    
    }
    
    // Perform the actual transfer provided all approvals have been received
    function executeTransfer(uint requestId) public onlyOwner validRequestId(requestId) returns (uint newBalance){
        Request storage requestToExecute = requests[requestId];
        
        require(requestToExecute.transferDone == false, "This transfer request was already executed");

          //If min number of approvals is reached, execute the transfer
        uint _approvals = getApprovals(requestId);
        require(_approvals >= minApprovals, "Missing approvals");
        
        requestToExecute.transferDone = true;
        
        uint oldBalance = address(this).balance;
        
        _transfer(requestToExecute);
        
        assert( (oldBalance - address(this).balance ) == requestToExecute.amount);
        
        return address(this).balance;
    
    }
    
     // Return the number of transfer requests that have not yet been executed
    function pendingRequests() public view returns (uint){
        
        uint _pending = 0;
        
        for(uint i=0; i< requests.length; ++i){
            if (requests[i].transferDone == false){
                _pending++;    
            }
        }
        return _pending;
    }
    
    
    // Return the number of approvals given by owners to the specified request
    function getApprovals(uint requestId)  public view validRequestId(requestId) returns (uint approvals){
        

        Request storage requestToApprove = requests[requestId];
        
        return requestToApprove.approvals.length;

    }

    // Return true if owner has approved the given request id
    function hasApproved(uint requestId, address owner) public view validRequestId(requestId) returns (bool approved){
        require(requestId < requests.length, "Invalid request id");
        
        Request storage requestToApprove = requests[requestId];

        for(uint i=0; i< requestToApprove.approvals.length; ++i){
            if (requestToApprove.approvals[i] == owner){
                return true;
            }
        }
        return false;
    }

    // Return the balance of the wallet
    function walletBalance() public view returns (uint balance){
        return address(this).balance;
    }

    function _transfer(Request storage request) private {
        
        require(request.amount <= address(this).balance, "Not enough balance to execute the transfer ");

        request.transferDone = true;
        
        request.recipient.transfer(request.amount);
        
    }
    
}



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

import "./OwnableSig.sol";

// I went ahead without watching the helping videos. I used my notes and other videos elsewhere.
// Here is my Multi-Sig Wallet

// there are three owners each with Owner ID and address
// anyone can deposit 
// only owners can withdraw - min 2 signatures with Owner Id and amount is required
// only owners can make transfers with address and amount inputs 

// signature signed with Owner ID - 1,2,3
// total signature count displays total valid number of signatures

contract Wallet is OwnableSig {
    
    
    mapping (address => uint) balance;
    
   
    event depositAdded(uint amount, address indexed depositedTo);
    event amountTransferred(uint amount, address indexed depositedTo, address depositedFrom, uint balance);
   
   
   
   function deposit() public payable returns (uint) { 
       balance[msg.sender] += msg.value;
       emit depositAdded(msg.value,  msg.sender);
       return balance[msg.sender];
   }
   
   function withdraw(uint amount, uint _ownerId) public returns (uint) { 
       // check there is enough balance // if conditions met // check signatures minimum met // send funds to owner's account
       require(balance[msg.sender] >= amount, "insuficient balance");
       require(owners[_ownerId].signatureCount > 0 &&  owners[_ownerId].signatureCount < ownersCount, "invalid number of required confirmations");
       balance[msg.sender] -= amount;
       msg.sender.transfer(amount);
       return balance[msg.sender];
        
   }

   
   function getBalance() public view returns (uint) {
       return balance[msg.sender];
   }
   
   function transfer(address recipient, uint amount) public checkMinSig() {
       require (balance[msg.sender] >= amount, "Your transfer exceeds the minimum amount necessary to complete transaction");
       require (msg.sender != recipient, "Transfer to same address is not allowed");
      
       uint prviousSenderBalance = balance[msg.sender];
       emit amountTransferred(amount, msg.sender, recipient, balance[msg.sender]);
       
       _transfer(msg.sender, recipient, amount);
       
       assert(balance[msg.sender] == prviousSenderBalance - amount);
       
   }
   
   function _transfer(address from, address to, uint amount) private { 
       balance[from] -= amount;
       balance[to] += amount;
   }
   

}


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

contract MinSigNum {
    
    uint public totalSigCount = 0;
    string message;
    bool result;
    
    modifier checkMinSig() {
        if(totalSigCount >= 2) {
            message = "Minimum number of signatures met!";
            (result, message);
        } else {
            revert("Amount of signatures required not met");
        }
        _;
    }
    
    
    function incrementsigCountFunc() internal {
        totalSigCount += 1;
    }
}

contract OwnableSig is MinSigNum {
    
    uint256 ownerId;
    
    struct Owners {
        uint256 id;
        string name;
        uint256 signatureCount;
        address _addr;
    }

    
    mapping(address => bool) signer;
    
    mapping(uint256 => Owners) public owners;
    
  
    uint256 public ownersCount;
    address private owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; 
    address private ownerB = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
    address private ownerC = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;
   

    
    constructor() { 
        addOwner("Owner 1", 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4);
        addOwner("Owner 2", 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        addOwner("Owner 3", 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db);
        owner = msg.sender;
        ownerB = msg.sender;
        ownerC = msg.sender;
        
    }
    
    
    function addOwner(string memory _name, address _addr) private {
        ownersCount ++;
        owners[ownersCount] = Owners(ownersCount, _name, 0, _addr);
        
    }
    
    function signature(uint256 _ownerId) public {
        if (signer[msg.sender] = true) {
                if(owners[_ownerId].signatureCount <= 0 ) {
                    incrementsigCountFunc();
                    owners[ownerId].signatureCount = totalSigCount;
            } else {
                revert("You've already signed. Wrong Owner");
            }
            
            }
            for(owners[_ownerId].signatureCount = 0; owners[_ownerId].signatureCount < 1; owners[_ownerId].signatureCount ++ ) {
                if(owners[_ownerId].signatureCount > 1) {
                    revert("Cannot sign twice");
            }

        }

    }
    
   
}
1 Like

How is everyone getting these nice print outs of code with colored numbers…and yes I know what you mean about mappings - i had the same discovery, lol.

2 Likes

With the “Preformatted text” </> button in the menu :slight_smile:

function withdraw(uint amount, uint _ownerId) public returns (uint) {
// check there is enough balance // if conditions met // check signatures minimum met // send funds to owner’s account
require(balance[msg.sender] >= amount, “insuficient balance”);
require(owners[_ownerId].signatureCount > 0 && owners[_ownerId].signatureCount < ownersCount, “invalid number of required confirmations”);
balance[msg.sender] -= amount;
msg.sender.transfer(amount);
return balance[msg.sender];

}


2 Likes

Hey @bjamRez, hope you are well.

Yes, has @codinginlondon has mention, by using the Preformatted text button </>.

You can use the “Preformatted Text” Button to encapsulate any kind of code you want to show.


function formatText(){

let words = “I’m a preformatted Text box, Please use me wisely!”

}

prefromatted_text-animated

Carlos Z.

1 Like

Thanks Carlos, I will try

text text tex t ext tex t tex. //////// //////
wow, it’s magic!

Hello,
I have a question about an aspect of the project solution. In the function below:

function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] == false);
        require(transferRequests[_id].hasBeenSent == false);
        
        approvals[msg.sender][_id] = true;
        transferRequests[_id].approvals++;
        
        emit ApprovalReceived(_id, transferRequests[_id].approvals, msg.sender);
        
        if(transferRequests[_id].approvals >= limit){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
            emit TransferApproved(_id);
        }
    }

If the transfer is automatically executed when the last approval is received, isn’t there a risk of having a scenario whereby: the transfer fails for whatever reason (network issue, missing balance) and consequently the whole transaction is reverted, including the approval of the last owner. The last owner would therefore have to approve again, although the transfer failure would not be related to his approval?

Matt

1 Like

What I don’t understand is how to reduce the balance of one of the owners that has sent funds to a different owner (balances of addresses that receive money always increase so that part works).
I added this line in my approve function:

mappingBalances[msg.sender] -= transferRequests[_id].amount;

That obviously works only with the address that deployed the contract (msg.sender) but not with the other 2 addresses that are in the owners array.

Could you explain how to fix that? The entire code is below, thanks.

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    address[] public owners;
    uint limit; 
    
    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }
    
    event TransferRequestCreated (uint _id, uint _amount, address _initiator, address _receiver);
    event ApprovalReceived (uint _id, uint _approvals, address _approver);
    event TransferApproved (uint _id);
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approvals; 
    
    modifier onlyOwners(){
        require (inOwnersArray() == true);
    _;
    }
    
    function inOwnersArray () private view returns (bool){ //additional function that checks if msg.sender is in the owners array.
        bool inArray;
        for (uint i = 0; i < owners.length; i++){
            if (owners[i] == msg.sender){
                inArray = true;
                break;
            }
            else {inArray = false;}
        }
        return inArray;
    }
   
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    function deposit() public payable { 
        mappingBalances[msg.sender] += msg.value;
    }
    
    //We keep track of balances that have been changed after transfer requests go through.
    mapping (address => uint) mappingBalances; 
    
    function getAllBalances () public view returns (uint){ 
        return mappingBalances[msg.sender];
        
    }
    
        
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        require(_amount <= mappingBalances[msg.sender]); // Require for the sender's balance to be able to cover the transfer.
        
        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);
        transferRequests.push(Transfer(_amount, _receiver, 0, false, transferRequests.length)); // 0 approvals at the start.
    }
    
    //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] == false);
        if (transferRequests[_id].approvals < limit){
            transferRequests[_id].approvals++;
            emit ApprovalReceived (_id, transferRequests[_id].approvals, msg.sender);
    }
            
            approvals[msg.sender][_id] = true;
            if (transferRequests[_id].approvals >= limit){
                transferRequests[_id].receiver.transfer(transferRequests[_id].amount); 
                transferRequests[_id].hasBeenSent = true;
                
                mappingBalances[transferRequests[_id].receiver] += transferRequests[_id].amount;
mappingBalances[msg.sender] -= transferRequests[_id].amount;
                
                emit TransferApproved (_id);
            }
        }
        
    
        //Should return all transfer requests
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    }
    
    
}
1 Like

Hi all, here is my attempt at the multisig wallet. Tried not to peak too much.

pragma solidity 0.7.5;
pragma abicoder v2;

contract MulitSigWallet {
    
    //mappings
    mapping(address => uint)balance;
    mapping(address => mapping(uint => bool)) hasVoted;
    
    //Golbal vars
    uint requiredConfirms;
    address[] public contractAdmins;
    
    struct Withdraw {
        address from;
        address to;
        uint amount;
        uint txId;
        uint confirms;
        bool txConfrimed;
    }
    
    
    Withdraw[] withdrawRequests;
   
    //modifiers
    modifier onlyAdmin(){
        bool isAdmin = false;
        
        for(uint i=0; i<contractAdmins.length;i++){
            if(contractAdmins[i] == msg.sender){
                isAdmin = true;
                }
            }
            require(isAdmin == true, "Error! Only the address owner can do this");
            _;
    }
    
    constructor(address[] memory _contractAdmins, uint _requiredConfirms) {
        contractAdmins = _contractAdmins;
        requiredConfirms = _requiredConfirms;
        
    }
    
    
    function makeWitdrawReq(address _from, address _to, uint _amount) payable public {
        withdrawRequests.push(Withdraw (_from, _to, _amount, withdrawRequests.length, 0, false) );
    }
    
    function approveWithdrawReq(uint _id) public onlyAdmin returns(Withdraw memory){
        require(hasVoted[msg.sender][_id] == false, "You cannot vote twice.");
        hasVoted[msg.sender][_id] = true;
        withdrawRequests[_id].confirms ++;
        
        if(withdrawRequests[_id].confirms >= requiredConfirms) {
            require(withdrawRequests[_id].txConfrimed == false);    
            _transfer(withdrawRequests[_id].from, withdrawRequests[_id].to, withdrawRequests[_id].amount);
            withdrawRequests[_id].txConfrimed = true;
        }
        return(withdrawRequests[_id]);
    }
    
    function _transfer(address from, address recipient, uint amount) private {
        require(from != recipient, "Don't sends money to yourself!");
        balance[from] -= amount;
        balance[recipient] += amount;
    }
    
    function getWithdrwReq(uint _id) public view returns(address, address, uint, uint, uint, bool){
        return(withdrawRequests[_id].from, withdrawRequests[_id].to, withdrawRequests[_id].amount, withdrawRequests[_id].txId, withdrawRequests[_id].confirms, withdrawRequests[_id].txConfrimed);
    }
    
    function deposit() public payable returns (uint) {
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }
    
    function getBalance()public view returns(uint) {
        return balance[msg.sender];
    }
    
    function getContractBalance() public view returns (uint bal) {
        return address(this).balance; // balance is "inherited" from the address type
    }
    // ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"]
}

looks better now @thecil thanks

1 Like

Hi @codinginlondon, hope you are ok.

If the function executed (transfer for your example) fails for any reason, the transaction will not be appended in the blockchain, so the data like an approval of the last owner will never execute, in that way the transaction was never approved, so the last owner has never sign the transfer, it will have to do it again.

If you have any more questions, please let us know so we can help you! :slight_smile:

Carlos Z.

1 Like

Hey @Andro, hope you are great.

Now about your error in your mappingBalances[msg.sender] -= transferRequests[_id].amount; , the problem is when you approve the transfer from another owner that is not the one who create the transaction request.

So when the 2nd owner, approve a transaction, the amount will be debited from the 2nd owner [msg.sender], not from the one who create the transaction request.

So i think to fix this, you might will need to redesign your struct and add another variable that should be something like “address payable requester” for example. Then you should be able to just debit the funds properly from the one who requested the transaction.
transferRequests[_id].requester

If you have any more questions, please let us know so we can help you! :slight_smile:

Carlos Z.

1 Like

Thanks @thecil :+1:
Matt

1 Like

Final code guys:

pragma solidity 0.7.5;

pragma abicoder v2;

contract MyWallet {
    
    address[] public walletOwners; // Wallet owner addresses 
    uint txtLimit; //Transaction signature limit
    
    struct txtTransfer{ // Transaction transfer struct 
        
        uint amount;
        address payable theReceiver;
        uint txtVotes;
        bool wasSend;
        uint txtId;
    }
    
    // Logging events
    event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _receiver);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);
    
    txtTransfer[] txtRequests;
    
    mapping(address => mapping(uint => bool)) txtVotes;
    
    
    modifier onlyOwners(){ //Check if owner is within the walletOwners array
        
        bool _walletOwner = false;
        uint _counter = 0;
        
        while (_walletOwner != true) { // Rotate throught he walletOwners array to see if the sender is among them
        
            if(walletOwners[_counter] == msg.sender) {
                
                _walletOwner = true;
            }
            
            else {
                
                _counter++;
            }
        }
            
        require(_walletOwner == true);
        _; // Continue execution 
    }
    
    /////////////////////////////////////////////////////////////////
    //Initialize txtLimit and walletOwners on contract startup
    /////////////////////////////////////////////////////////////////    
    
    constructor(address[] memory _walletOwners, uint _txtLimit) {
        walletOwners = _walletOwners;
        txtLimit = _txtLimit;

    }
    
    function deposit() public payable {// Deposit function
        // Empty body function
    }
    
    function createTransfer(uint _amountToSend, address payable _receiver) public onlyOwners {//Initialize the txtTransfer struct and add it to the transferRequests array
        emit TransferRequestCreated(txtRequests.length, _amountToSend, msg.sender, _receiver);
        txtRequests.push(txtTransfer(_amountToSend,_receiver,0,false,txtRequests.length));
    }
    
    function checkVotes(uint _id) public onlyOwners {
        require(txtVotes[msg.sender][_id] == false);
        require(txtRequests[_id].wasSend == false);
        
        txtVotes[msg.sender][_id] = true;
        txtRequests[_id].txtVotes++;
        
        emit ApprovalReceived(_id, txtRequests[_id].txtVotes, msg.sender);
        
        if(txtRequests[_id].txtVotes >= txtLimit){
            txtRequests[_id].wasSend = true;
            txtRequests[_id].theReceiver.transfer(txtRequests[_id].amount);
            emit TransferApproved(_id);
        }
    }
    
    //Should return all transfer requests
    function getTransferRequests() public view returns (txtTransfer[] memory){
        return txtRequests;
    }
}
1 Like

Thanks, I’ll try to add it to the code.

1 Like

Here is my project, I tested it and it works but I’m not sure if best practices are in place :grimacing:.
It consists of 3 contracts, when the Wallet contract is deployed it takes the input of how may signers are required to validate a transaction.

Wallet.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.7.4;

import "./Validate.sol";


contract Wallet is Validate {
    
    constructor(uint ownersRequired) 
    Validate(ownersRequired)
    {
        
    }
    
    function proposeTransfer(address payable to, uint256 amount) public onlyOwner {
        require(amount <= balance, "insufficient wallet balance");
        Validate._proposeTransfer(to, amount);
    }
    
    function bal() public view returns(uint256) {
        return Validate.balance;
    }
    
    function sign() public onlyOwner {
        Validate._sign();
    }
    
    function addOwner(address newOwner) public onlyOwner {
        Ownable._addOwner(newOwner);
    }
    
    function ownerCount() public view returns(uint256) {
        return Ownable._owners.length;
    }
    
    function _signers() public view returns(uint256) {
        return Validate.signers.length;
    }
}


Validate.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.7.4;

import "./Ownable.sol";


contract Validate is Ownable {
    
    event TransferMade(address to, uint256 amount);
    
    uint256 internal balance;
    uint256 _ownersRequired;
    address[] signers;
    struct Transfer {
        address payable to;
        uint256 amount;
    }
    Transfer newTransfer;
    bool transferExists = false;
    
    constructor(uint256 ownersRequired)
    {
        _ownersRequired = ownersRequired;
    }
    
    function deposit() public payable {
        balance += msg.value;
    }
    
    function _proposeTransfer(address payable to, uint256 amount) internal onlyOwner {
        transferExists = true;
        newTransfer = Transfer(to, amount);
        _sign();
    }
    
    function _sign() internal onlyOwner {
        require(transferExists == true, "no transfer proposed");
        bool signed = false;
        
        for(uint256 i = 0; i < signers.length; i++) {
            if(msg.sender == signers[i]) {
                signed = true;
            }
        }
        
        require(signed == false, "already signed by this owner");
        signers.push(msg.sender);
        
        if(signers.length >= _ownersRequired) {
            confirmTransfer();
        }
    }
    
    function confirmTransfer() private {
        address payable to = newTransfer.to;
        uint256 amount = newTransfer.amount;
        
        to.transfer(amount);
        balance -= amount;
        transferExists = false;
        delete signers;
        
        emit TransferMade(to, amount);
    }
    
}

Ownable.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.7.4;


contract Ownable {
    
    address[] internal _owners;
    
    constructor(){
        _owners.push(msg.sender);
    }
    
    modifier onlyOwner {
        require(checkAddress(), "must be owner to call function");
        _;
    }
    
    function _addOwner(address newOwner) internal onlyOwner {
        bool check = false;
        
        for(uint256 i = 0; i < _owners.length; i++) {
            if(newOwner == _owners[i]) {
                check = true;
            }
        }
        require(check == false, "address is already owner");
        _owners.push(newOwner);
    }
    
    function checkAddress() private view returns(bool) {
        bool check = false;
        
        for(uint256 i = 0; i < _owners.length; i++) {
            if(msg.sender == _owners[i]) {
                check = true;
            }
        }
        
        return check;
    }
}
1 Like

Hi, I write three contract: Ownable, Balance and Transaction. On the contract Ownable, I write variables like owner address and required minial limit for the transaction approval. There is a function “register” to add owner. We can also use the contract to judge if an address is one of the owner.

pragma solidity 0.7.5;
contract Ownable{

address owner;
address[] owners;
uint limit;

constructor(){
    owner =msg.sender;
    owners.push(msg.sender);        

}  
 
function register (address MsgSender) internal {
    owners.push(MsgSender);        
    limit = owners.length*2/3;
}

function isOwner(address MsgSender) internal view returns(bool) {
    uint i = 0;
    bool isowner = false;
    while(i < owners.length){
        if (MsgSender == owners[i]){
             isowner=  true;
             break;
        }
    i++;
    }
    return isowner;
}

modifier OnlyOwners {
    require ( isOwner(msg.sender) == true, "message sender should be the owner." );
_;
}

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

function getlimit() public view returns(string memory,uint, string memory,uint) {
    return ("limit",limit,"owners", owners.length);
} 

}

Then in the contract “Balance”, an address can deposit money. And during the process of deposit, it will register as owner. It cannot double register since the if condition.

pragma solidity 0.7.5;
import “./Ownable.sol”;
contract Balance is Ownable{

mapping(address => uint) balance;

event Deposit (uint amount, address indexed depositedTo );

function deposit() public payable returns(uint){
    balance[msg.sender] += msg.value;
    emit Deposit (msg.value, msg.sender);
    if (isOwner(msg.sender) == false){
        register(msg.sender);
        }
    return balance[msg.sender];}

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

}

The contract “Transaction” is the main body of our multsig bank. We can add a transfer request in the function “AddTransfer” , approve a transfer request in the function “approve”, confirm a transfer in “ConfirmTransfer”. After the confirmation, the transfer request will be deleted in the array “TransferRequests”.
contract Transaction is Balance{

struct Transfer{
    address sender;
    address recipient;
    uint amount;
    }

Transfer[] TransferRequests; 

modifier SufficientBalance(uint _amount) {
    require(balance[msg.sender] >= _amount,"The balance hasn't enough money.");
    _;
    }  

modifier SelfTransfer(address _recipient){
    require(msg.sender != _recipient, "You cannot send money to yourself.");
    _;
    }

modifier RightOrder(uint order){
    require( order <= TransferRequests.length, "You need to enter a right order name");
    require (1<=order, "You need to enter a right order name");
    _;
    }    

function AddTransfer(address _recipient, uint _amount) OnlyOwners SelfTransfer(_recipient) SufficientBalance(_amount) public {
    Transfer memory tran ;
    tran.sender = msg.sender;
    tran.recipient =_recipient;
    tran.amount = _amount;
    TransferRequests.push(tran);
    } 

function getTransferRequests(uint order) RightOrder(order) public view  returns( string memory, address, string memory, address, uint){
    return ("from:", TransferRequests[order-1].sender, "To:", TransferRequests[order-1].recipient,TransferRequests[order-1].amount );
    }

function _transfer(address from, address to, uint amount ) private{
    balance[from] -= amount;
    balance[to] += amount;
    } 

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

function approve(uint order, bool _approve) OnlyOwners RightOrder(order) public  {
    approvals[msg.sender][order-1] =  _approve;
    }
 
function TransferState(uint order) RightOrder(order) public view returns( uint){
    uint ApprovalNumber = 0;
    uint i = 0;
    while(i < owners.length){
        if (approvals[owners[i]][order-1] == true){
             ApprovalNumber++;   
            }
        i++;
    }
return ApprovalNumber;  //The approval number of this transaction
    }

function ConfirmTransfer(uint order) public RightOrder(order) returns (string memory){ 
    uint _ApproveNumber = TransferState(order);
    if (_ApproveNumber >= limit){
        uint oldSenderBalance = balance[TransferRequests[order-1].sender];
                     _transfer(TransferRequests[order-1].sender, TransferRequests[order-1].recipient, 
                    TransferRequests[order-1].amount);
        assert(balance[TransferRequests[order-1].sender] == oldSenderBalance - 
                   TransferRequests[order-1].amount);
        delete TransferRequests[order-1];
        return "The transfer is approved";
        }
    else{
        require(_ApproveNumber >= limit ); 
        return "The transfer is not approved";         
        }     
    }

}
1 Like

I’ve got an issue with my attempt. When I invoke requestTransfer() I receive the following error saying I need to make function payable. I can’t see why I need to do that as I am not sending an ETH value. Even setting the payable modifier in the function header still results in the same error:

transact to MultiSigWallet.requestTransfer errored: VM error: revert. revert 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.
pragma solidity 0.8.0;
pragma abicoder v2;

import "./Ownable.sol";

contract MultiSigWallet {
    
    uint balance;
    uint approvalLimit;
    
    //mapping that is checked when an address attempts to approve a transaction.
    mapping(address => bool) owners; 
    
    modifier anyOwner {
        require(owners[msg.sender] == true, "you are not an owner of this wallet!");
        _;
    }
    
    struct Tx {
        address initiator;
        address payable recipient;
        uint amount;
        mapping(address => bool) approvers;
        uint approvals;
    }
    
    Tx[] txs;
    
 
    event deposited(address indexed sender, uint amount);
    event deployed(address indexed owner);
    event transferRequest(address indexed initiator, address indexed recipient, uint amount);
    event transactionApproved(address indexed approver, uint transaction);
    event transferred(address indexed recipient, uint amount);
    
    constructor(uint _approvalLimit, address[] memory _owners) {
        require(_approvalLimit > 0, "at least one approver is required for multisig");
        require(_owners.length >= _approvalLimit, "not enough owners to satisfy approval limit!");
        approvalLimit = _approvalLimit;
        owners[msg.sender] = true;
        emit deployed(msg.sender);
        
        for (uint8 i = 0; i<_owners.length; i++) {
            owners[_owners[i]] = true;
            emit deployed(_owners[i]);
        }
    }
    
    
    function deposit() public payable {
        
        balance += msg.value;
        emit deposited(msg.sender, msg.value);
    }
    
    function requestTransfer(address payable recipient, uint amount) public anyOwner returns (uint) {
        require(balance >= amount, "You do not have enough funds to perform this transfer");
        require(msg.sender != recipient, "sender and recipient of this transfer are the same!");
        Tx storage tx = txs[txs.length];
        tx.initiator = msg.sender;
        tx.recipient = recipient;
        tx.amount = amount;
        emit transferRequest(msg.sender, recipient, amount);
        return txs.length - 1;
    }
    
    
    function _transfer(Tx storage transaction) private returns (uint) {
        require(balance >= transaction.amount, "You do not have enough funds to perform this transfer");
        transaction.recipient.transfer(transaction.amount);
        balance -= transaction.amount;
        emit transferred(transaction.recipient, transaction.amount);
        return balance;
    }
    
    function approveTransfer(uint transactionId) public anyOwner {
        require(transactionId < txs.length, "transaction with supplied id does not exist!");
        require(txs[transactionId].approvals < approvalLimit, "transaction has already been approved and transferred!");
        require(txs[transactionId].approvers[msg.sender] == false, "you have already approved this transaction!");
        Tx storage tx = txs[transactionId];
        tx.approvals += 1;
        
        if (tx.approvals == approvalLimit) {
            _transfer(tx);
        } 
    }
}
1 Like

You should check your _transfer function, you ask for a recipient but you never send him the argument of which recipient. Maybe you should rewatch the videos to get the proper idea for the _transfer function.

    function _transfer(Tx storage transaction) private returns (uint) {
        require(balance >= transaction.amount, "You do not have enough funds to perform this transfer");
        transaction.recipient.transfer(transaction.amount);
        balance -= transaction.amount;
        emit transferred(transaction.recipient, transaction.amount);
        return balance;
    }

If you have any more questions, please let us know so we can help you! :slight_smile:

Carlos Z.