Project - Multisig Wallet

  • Please post your questions and answers regarding the Multisig Wallet Project below.
10 Likes

I havenā€™t watched past the first video as I thought Iā€™d have a crack at it solo and hopefully get some peer review.

Contract is created with 3 different signatory addresses.
Only signatories create the transactions, which includes 1 signature of the creating signatory.
Only signatories can sign the transactions.
Upon the 2nd signature signing, the transfer of funds in processed

Fix: Spelling mistakes
Fix: Now only deals with wei inputs to remove decimals

WARNING! Only a signatory can change a listed signatory address (could cause issues with a rogue signatory stripping power from the others and taking control, controlling all 3 signatory addresses, potentially defeating the point of the multisig)

// "SPDX-License-Identifier: AGPL-3.0"
pragma solidity 0.7.5;
//pragma experimental ABIEncoderV2;

// 2of3 Multisig wallet.
// Receives from any address, requires 2 of 3 signatories send

// JS VM top 3 addresses for deployment use - 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db

contract MultiSig {
    struct Transaction {
        uint id;
        address multisig0;
        address multisig1;
        address recipient;
        uint amount;
        uint confs;
    }
    
    Transaction[] private transactions;
    
    uint private balance; // actual account balance
    uint private pendingBalance; // account balance after pending transactions
    
    address[3] private multisig;
    
    event newTransaction(uint id, address signatory, address to, uint amount);
    event newTransactionConf(uint id, address signatory);
    event paymentTransfer(uint id, address to, uint amount);
    
    constructor(address _multisig0, address _multisig1, address _multisig2) {
        // Sanity check, must be 3 different signatory addresses
        require(_multisig0 != _multisig1 && _multisig0 != _multisig2 && _multisig1 != _multisig2, "Must have 3 different signatory addresses.");
        
        multisig = [_multisig0, _multisig1, _multisig2]; // Set initial multisig addresses on creation
        balance = 0;
        pendingBalance = 0;
    }
    
    modifier onlyOwner() {
        require(msg.sender == multisig[0] || msg.sender == multisig[1] || msg.sender == multisig[2], "Function not initiated by contract owner.");
        _; // If true, continue execution
    }

    function setMultisig(uint _index, address _newMultisig) public onlyOwner {
        multisig[_index] = _newMultisig;
    }
    
    function getMultisig() public view returns(address[3] memory)
    {
        return multisig;
    }
    
    function getBalance() public view returns(uint) {
        return balance;
    }
    
    function getPendingBalance() public view returns(uint) {
        return pendingBalance;
    }
    
    function receiveFunds() public payable{
        balance += msg.value;
        pendingBalance += msg.value;
    }
    
    function createTransaction(address _recipient, uint _amount) public onlyOwner {
        // Create new transaction struct
        Transaction memory _newTransaction;
        
        require(msg.sender != _recipient, "Cannot send to signatories.");
        require(balance >= _amount && pendingBalance >= _amount, "Not enough funds to transact.");
        
        _newTransaction.id = transactions.length;
        _newTransaction.multisig0 = msg.sender;
        _newTransaction.multisig1 = address(0x0);
        _newTransaction.recipient = _recipient;
        _newTransaction.amount = _amount;
        _newTransaction.confs = 1;
        
        // Push the new Transaction struct to the transactions array
        transactions.push(_newTransaction);
        
        pendingBalance -= _amount;
        
        emit newTransaction(_newTransaction.id, msg.sender, _newTransaction.recipient, _newTransaction.amount);
    }
    
    function confirmTransaction(uint _id) public onlyOwner {
        // confirm transaction exists with only 1 signatory confimation
        require(transactions[_id].confs < 2, "Transaction already confirmed.");
        
        // <pedantic checking>
        // confirm multisig0 is not a 0 address
        require(transactions[_id].multisig0 != address(0x0), "!!Error: MultiSig0 is a 0 address!!");
        
        // confirm multisig1 is a 0 address
        require(transactions[_id].multisig1 == address(0x0), "!!Error: MultiSig1 is not a 0 address!!");
        // </pedantic checking>
        
        // confirm msg.sender has not already confirmed the transaction
        require(msg.sender != transactions[_id].multisig0, "Transaction already signed by this signatory.");
        
        transactions[_id].multisig1 = msg.sender;
        transactions[_id].confs = 2;
        
        emit newTransactionConf(_id, msg.sender);
        
        processTransaction(transactions[_id].id, transactions[_id].recipient, transactions[_id].amount);
    }
    
    function getTransaction(uint _id) public view returns(uint id, address multisig0, address multisig1, address recipient, uint amount, uint confs) {
        return(transactions[_id].id, transactions[_id].multisig0, transactions[_id].multisig1, transactions[_id].recipient, transactions[_id].amount, transactions[_id].confs);
    }
    
    function processTransaction(uint _id, address _recipient, uint _amount) public payable onlyOwner {
        address payable sendTransfer = address(uint160(_recipient));
        balance -= _amount;
        
        sendTransfer.transfer(_amount); // On error revert
        
        emit paymentTransfer(_id, _recipient, _amount);
    }
    
    function toWei(uint _ether) public pure returns(uint) {
        uint _wei = _ether * 1e18;
        
        return _wei;
    }
}
4 Likes

pragma solidity 0.7.5;
pragma abicoder v2;

contract MultisigWallet {

struct PermissionSet {
    address SignerOne;
    uint TxIDOne;
    address SignerTwo;
    uint TxIDTwo;        
    address SignerThree;
    uint TxIDThree;
    uint TxType;
    uint TXAmount;
}



constructor (){
    
    struct PermissionSet DefaultPermissions {msg.sender, 0, msg.sender, 0, msg.sender, 0, 0, 0}
    struct PermissionSet DraftPermissions {msg.sender, 0, msg.sender, 0, msg.sender, 0, 0, 0}

    uint[] SignedTXList {}
    
}

address newSignatory;
address oldSignatory;
address transferree;


event depositDone (uint amount, address indexed receivedFrom);
event transferDone (uint amount, address indexed transferredTo );
event signatureReceived (uint transaction, address indexed signatory);
event singnatoryChanged (uint transaction, address indexed oldSignatory, address indexed newSignatory);


modifier isSigner() {
    require ((msg.sender == DraftPermissions.SignerOne ||
                msg.sender == DraftPermissions.SignerTwo ||
                msg.sender == DraftPermissions.SignerThree), "Caller must be a signatory");
                
    _;
}

modifier isSigned() {
    CurrentTX = SignedTXList.length;
    uint permissions = 0;
    if (DraftPermissions.TXIDOne == CurrentTX){
        permissions ++;
    }
    if (DraftPermissions.TXIDTwo == CurrentTX){
        permissions ++;
    }
    if (DraftPermissions.TXIDThree == CurrentTX){
        permissions ++;
    }
    assert (DraftPermissions.TxType != 0, "Transaction type not defined");
    require (permissions >=2, "Not enough signatures.");
    _;
}

function deposit() public payable returns (uint deposit) {
    emit depositDone (msg.value, msg.sender);
    return (msg.value);
    
}

function getCurrentSignableTX() public view returns (unint TXID, uint TXType, uint amount) {
    return (SignedTXList.length, DraftPermissions.TXType, DraftPermissions.TXAmount)
} 

function signCurrentTransaction (uint transactionID, uint txType, unit txAmount) public isSigner {
    require(transactionID == SignedTXList.length, "Please get current signable txn ID")
    require(txType == DraftPermissions.TxType, "Please get current signable txn type");
    require(txAmount == DraftPermissions.TXAmount, "Please currect transferrable amt");
    if (DraftPermissions.SignerOne == msg.sender) {
        DraftPermissions.TXIDOne = SignedTXList.length;
    } 
    if (DraftPermissions.SignerTwo == msg.sender) {
        DraftPermissions.TXIDTwo = SignedTXList.length;
        
    }
    if (DraftPermissions.SignerThree == msg.sender) {
        DraftPermissions.TXIDThree = SignedTXList.length;
        
    }
    emit signatureReceived (transactionID, msg.sender);
}

function proposeTransfer (address transferTo, uint amount) public {
    require (amount <= this.balance, "Insufficient funds for proposed transfer");
    transferree = transferTo
    DraftPermissions = DefaultPermissions;
    DraftPermissions.TxType = 1;
    DraftPermissions.TxAmount = amount;
    
}

function finalizeTransfer (address transferTo, uint amount) public isSigner isSigned {
    require (amount <= this.balance, "Insufficient funds for proposed transfer");
    require (transferree == transferTo, "Wrong address being transferred to");
    transferTo.transfer(amount);
    emit transferDone (amount, transferTo);
    DraftPermissions = DefaultPermissions;
}


function proposeChangeSignatory(address toChange, address changeTo) public {
    require( (DraftPermissions.SignerOne == toChange ||
                DraftPermissions.SignerTwo == toChange ||
                DraftPermissions.SignerThree == toChange), "Signer not correct");
    DraftPermissions = DefaultPermissions;
    DraftPermissions.TxType = 2;// Type 1 is Transfer and Type 2 is Signatory Chang
    oldSignatory = toChange;
    newSignatory = changeTo;
    
}

function ChangeSignatory() public isSigner isSigned {
    emit singnatoryChanged(SignedTxList.length, oldSignatory, newSignatory);
    if (DraftPermissions.SignerOne == oldSignatory) {
        DefaultPermissions.SignerOne = newSignatory;
        oldSignatory = newSignatory;
        SignedTxList.push(DraftPermissions.TxType);
    }
    if (DraftPermissions.SignerTwo == oldSignatory) {
        DefaultPermissions.SignerTwo = newSignatory;
        oldSignatory = newSignatory;
        SignedTxList.push(DraftPermissions.TxType);
    }
    if (DraftPermissions.SignerThree == oldSignatory) {
        DefaultPermissions.SignerThree = newSignatory;
        oldSignatory = newSignatory;
        SignedTxList.push(DraftPermissions.TxType);
    }
    DraftPermissions = DefaultPermissions;
    

}

}

3 Likes

Hi There, here is my contract.

I would appreciate some feedback and merry christmas all!

pragma solidity 0.8.0;

contract multiSigWallet{
    
    //This contract is a multisignature wallet requiring 2/3 signatures to execute a transaction.

  uint private TotalBalance;

  //Owners
  address private Owner1 = address(0);
  address private Owner2 = address(0);
  address private Owner3 = address(0);

  //Transaction Struct
  struct Atransaction {
    address payable recipient;
    uint amount;
    address initiator;
    bool signature1;
    address countersigner;
    bool signature2;
    bool status; //True is a completed-executed transaction, False is a pending transaciton.
  }

  //Array to log transactions
  Atransaction[] private TransactionLog;

  //Constructor
  constructor() {
    Owner1 = msg.sender;
  }

  //Modifiers
  modifier OnlyAnOwner() {
    require(msg.sender == Owner1 || msg.sender == Owner2 || msg.sender == Owner3, "You are not a Wallet Owner!");
    _; //run function
  }

  //OneTime Function
  function SetOtherOwners(address _Owner2, address _Owner3) public {
    require(msg.sender == Owner1, "You are not the wallet initiator!, You do not have access to define other wallet owners");
    require(Owner2 == address(0) && Owner3 == address(0), "Wallet Owners are already defined.");
    Owner2 = _Owner2;
    Owner3 = _Owner3;
  }

  //Events
  event depositDone(address indexed depositedBy, uint ammount);
  event TransactionStarted(address indexed intiator, address indexed recipient, uint amount);
  event signatureDone(uint indexed transaction, address indexed Signatore, bool approval);
  event transactionDone(uint index, address indexed recipient, uint amount);

  //Functions
  function getATransaction(uint index) public view OnlyAnOwner returns(Atransaction memory){
    return (TransactionLog[index]);
  }

  function SignATransaction(uint index, bool approval, bool send) public OnlyAnOwner {
      //Checks
    require(TransactionLog[index].status != true, "This transaction is already completed-executed.");
    require(msg.sender != TransactionLog[index].initiator,"You are the transaction initiator!, another owner must countersign");
    
    //Update Signatures
    TransactionLog[index].countersigner = msg.sender;
    TransactionLog[index].signature2 = approval;

    emit signatureDone(index, msg.sender, approval);
    
    //If signer chose to execute transaction execute it.
    if (send) {
      ExecuteTransaction(index);
    }
  }

  function InitializeTransaction(address payable _recipient, uint _amount) public OnlyAnOwner {
    Atransaction memory newTransaction;
    newTransaction.recipient = _recipient;
    newTransaction.amount = _amount;
    newTransaction.initiator = msg.sender;
    newTransaction.signature1 = true;
    newTransaction.countersigner = address(0);
    newTransaction.signature2 = false;
    newTransaction.status = false;

    insertTransaction(newTransaction);
    emit TransactionStarted(msg.sender, _recipient, _amount);
  }

  function insertTransaction (Atransaction memory newTransaction) private {
    TransactionLog.push(newTransaction);
  }

  function ExecuteTransaction(uint index) public payable OnlyAnOwner{
    //Checks
    require(TransactionLog[index].status != true, "This transaction is already completed-executed. Initialize a new transaction if required.");
    require(TransactionLog[index].signature1 == true && TransactionLog[index].signature2 == true, "Not enough Signatures!" );
    require(TotalBalance >= TransactionLog[index].amount, "Not sufficient funds");
    
    
    //Transfer and Balance Update
    TransactionLog[index].recipient.transfer(TransactionLog[index].amount);
    TotalBalance -= TransactionLog[index].amount;
    
    //Transaction Object Update
    TransactionLog[index].status = true;
    
    emit transactionDone(index,TransactionLog[index].recipient, TransactionLog[index].amount);
  }

  function deposit() public payable returns(uint) {
    TotalBalance += msg.value;
    emit depositDone(msg.sender, msg.value);
    return TotalBalance;
  }


  function getOwners() public view OnlyAnOwner returns(address, address, address){
    return (Owner1, Owner2, Owner3);
  }

  function getWalletBalance() public view returns(uint){
    return TotalBalance;
  }

}
3 Likes

This is my multisig wallet, I used the given template at the start. Within the constructor I added error checking to verify the input to ensure the user was only giving valid information (more than 1 address, no duplicates, approvals canā€™t exceed number of owners).

For the duplicate check I ran nested loops and toggled a boolean if a duplicate was found and since thereā€™s no need to continue looping the list in that case, I break (hopefully saving some gas). If thereā€™s a better way to do this, please let me know.

I also implemented my (potential) gas saving trick of utilizing ā€˜break;ā€™ within my onlyOwners() modifier. The rest of the contract is pretty straight forward, thanks for checking it out and let me know what you think.

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    address[] internal owners;
    uint limit; 
    
    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }
    
    Transfer[] transferRequests;
    mapping(address => mapping(uint => bool)) approvals;
    
    modifier onlyOwners(){
        
       bool _amOwner;
        for (uint i=0; i <= owners.length - 1 ; i++)
       {
            if (owners[i] == msg.sender) {
                _amOwner = true;
                break;
            }
        }
        require(_amOwner,"Unauthorized");
        _;
   }
   
    constructor(address[] memory _owners, uint _limit){
        
        require(_owners.length > 1 , "More than one address required.");
        require(_limit > 1 , "More than one signature required.");
        require(_limit <= _owners.length, "Total number of signatures required can't exceed total number provided.");
        
        // Check for duplicate addresses
        bool _duplicateFound;
        for (uint i=0; i <= _owners.length - 1; i++)
        {
            for (uint j = i + 1; j < _owners.length; j++)
            {
                if (_owners[i] == _owners[j]){
                    _duplicateFound = true;
                    break;
                }

            }
            
            if (_duplicateFound == false)
              owners.push(_owners[i]);
        }
        require(_duplicateFound == false, "Duplicate address found.");
        limit = _limit;
    }

    function deposit() public payable{}

      //  -- Transfer struct attributes -- 
      //        uint amount;
      //        address payable receiver;
      //        uint approvals;
      //        bool hasBeenSent;
      //        uint id;
    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.");
        
        transferRequests.push(Transfer(_amount, _receiver, 0, false, transferRequests.length));
    }

    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++;
        
        if(transferRequests[_id].approvals >= limit){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
        }
   }

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

So, I only watched the first video to get the assignment and have a crack from there. Filip was not very specific about the amount of signers needed, he only gave an example of 3 people and needing at least 2 signature. So I decided to make it more dynamic and allow an infinite amount of signers.

My requirements:

  • A board can have an infinite amount of members
  • A board vote count is the minimum needed to achieve majority consensus
  • A board can only be edited by the owner (CEO perhaps?)
  • A wallet is connected to a board (this way, 2 board can manage multiple wallets).
  • A transaction is not automatically sent after final approval because the contract balance might be too small at the time. We do not want the signature for the transfer to be reverted because of this.
  • We allow a transaction to be performed with approvals of board members which are no longer on the board at the moment of transfer (we trust their intentions were valid at the time of signing).

multisigwallet

  1. A Board contract which keeps track of board members and calculates how mane votes would be needed for a majority.
  2. A MultiSigWallet which has a board attached to determine approvals

The Board contract is Ownable to make sure only the owner (CEO?) can hire/fire boardmembers. The MulltiSigWallet is BoardManaged meaning its access is determined by the board.

This way we abstract the management of access away from the wallet itself and make it dynamic so the wallet does not need any updates when the board changes over time.


Filestructure

/MultiSigWallet
    |- Board
        |- Board.sol
        |- BoardInterface.sol
        |- Ownable.sol
    |- BoardManaged.sol
    |- MultiSigWallet.sol

Board.sol

pragma solidity 0.7.5;

import './Ownable.sol';

contract Board is Ownable
{
    address[] members;
    
    event memberHired(address indexed newMember);
    event memberFired(address indexed firedMember);
    
    constructor() Ownable()
    {
        members.push(msg.sender);
    }
    
    /**
     * Hire a new board member to allow signing of transactions
     **/
    function hireMember(address _member) public onlyOwner
    {
        for(uint i = 0; i < members.length; i++) {
            require(members[i] != _member, "This member is already on the board.");
        }
        
        uint oldMemberCount = members.length;
        members.push(_member);
        
        assert(members.length == oldMemberCount + 1);
        
        emit memberHired(_member);
    }
    
    /**
     * Fire a board member so it can no longer sign transactions
     **/
    function fireMember(address _member) public onlyOwner
    {
        // Make sure the member is actually on the board, and get the index for it
        uint memberIndex = 0;
        bool memberExists = false;
        for(uint i = 0; i < members.length; i++) {
            if (members[i] == _member) {
                memberIndex = i;
                memberExists = true;
            }
        }
        require(memberExists, "Member is not on the board so it can not be fired.");
        
        uint oldMemberCount = members.length;
        // Clean up the member from the board list.
        for (uint i = memberIndex; i < members.length-1; i++){
            members[i] = members[i+1];
        }
        members.pop();
        
        assert(members.length == oldMemberCount - 1);
        
        emit memberFired(_member);
    }
    
    /**
     * Check if a member is on the board
     **/
    function isOnBoard(address _member) external view returns(bool)
    {
        for(uint i = 0; i < members.length; i++) {
            if (members[i] == _member) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Calculate the minimum amount of board members to approve a vote
     **/
    function getMajorityVoteCount() external view returns(uint)
    {
        // Initialize
        uint memberCount = members.length;
        uint multiplier = 10;
        
        // Find smallest multiplier needed
        while(memberCount > multiplier) {
            multiplier *= 10;
        }
        
        // Calculate half the votes
        uint calculated = memberCount * multiplier / 2;
        uint voteCount = multiplier;
        
        // Find first multiplier larger then half the votes for majority
        // Notice the '=' because for even number of members, an extra swingvote is needed
        for(uint i = 1; voteCount <= calculated; i++){
            voteCount = i * multiplier;
        }
        
        // Back to normal without multiplier
        voteCount /= multiplier;
        
        // Calculate majority
        return (voteCount);
    }
}

MultiSigWallet.sol

pragma solidity 0.7.5;
import './BoardManaged.sol';

contract MultiSigWallet is BoardManaged
{
    uint balance;
    Transaction[] transactions;
    
    struct Transaction {
        uint index;
        address recipient;
        uint amount;
        address[] signedBy;
        bool approved;
    }
    
    event deposited(uint amount, address indexed sender);
    event transactionPrepared(uint indexed index, address indexed recipient, uint amount, address indexed performedBy);
    event transactionSigned(uint indexed index, address indexed performedBy, uint signCount);
    event transactionPerformed(uint indexed index, address indexed performedBy);
    
    constructor(address _board) {
        board = BoardInterface(_board);
    }
    
    /**
     * Allow anyone to deposit eth into the contract
     **/
    function deposit() public payable
    {
        require(msg.value > 0);
        uint oldBalance = balance;
        balance += msg.value;
        assert(balance == oldBalance + msg.value);
        
        emit deposited(msg.value, msg.sender);
    }
    
    /**
     * Add new transaction which needs to be multi-signed and add sender as first signee.
     **/
    function prepareTransaction(address _recipient, uint _amount) public onlyBoard returns(uint)
    {
        require(_amount > 0, "Can only send more then 0 wei.");
        
        Transaction memory transaction;
        transaction.index = transactions.length;
        transaction.recipient = _recipient;
        transaction.amount = _amount;
        transaction.approved = false;
        transactions.push(transaction);
        transactions[transaction.index].signedBy.push(msg.sender);
        
        emit transactionPrepared(transaction.index, transaction.recipient, transaction.amount, msg.sender);
        
        return(transaction.index);
    }
    
    /**
     * Sign a transaction as boardmember
     **/
    function signTransaction(uint _transactionIndex) public onlyBoard
    {
        require(transactions[_transactionIndex].approved == false, "This transaction has already been done.");
        
        // Make sure this board member has not yet signed.
        for(uint i = 0; i < transactions[_transactionIndex].signedBy.length; i++) {
            require(transactions[_transactionIndex].signedBy[i] != msg.sender, "You have already signed this transaction.");
        }
        
        uint voteCount = transactions[_transactionIndex].signedBy.length;
        transactions[_transactionIndex].signedBy.push(msg.sender);
        
        assert(transactions[_transactionIndex].signedBy.length == voteCount + 1);
        
        emit transactionSigned(_transactionIndex, msg.sender, transactions[_transactionIndex].signedBy.length);
    }
    
    /**
     * Perform the actual transaction
     **/
    function performTransaction(uint _transactionIndex) public onlyBoard
    {
        uint minimumApprovals = board.getMajorityVoteCount();
        uint currentApprovals = transactions[_transactionIndex].signedBy.length;
        
        require(balance >= transactions[_transactionIndex].amount, "There is not enough balance in the contract.");
        require(currentApprovals >= minimumApprovals, "There not enough approvals.");
        require(transactions[_transactionIndex].approved == false, "This transaction has already been done.");
        
        address payable recipient = payable(transactions[_transactionIndex].recipient);
        uint oldBalance = balance;
        balance -= transactions[_transactionIndex].amount;
        transactions[_transactionIndex].approved = true;
        recipient.transfer(transactions[_transactionIndex].amount);
        
        assert(transactions[_transactionIndex].approved == true);
        assert(balance == oldBalance - transactions[_transactionIndex].amount);
        
        emit transactionPerformed(_transactionIndex, msg.sender);
    }
}

The full project code can be seen and forked here: https://github.com/ChristianVermeulen/MultiSigWallet


@filip I would love your feedback on this! :smiley:

13 Likes

Instead of using break, what i did was create a function which checks isOwner() and returns immediately once it is found.

function isOnBoard(address _member) external view returns(bool)
    {
        for(uint i = 0; i < members.length; i++) {
            if (members[i] == _member) {
                return true;
            }
        }

        return false;
    }

This could be called in the onlyOwner modifier as a single require:

modifier onlyBoard()
    {
        require(board.isOnBoard(msg.sender), "Address is not a board member");
        _;
    }

Just a different approach to the same problem :smiley:

4 Likes

@seer Thanks for the share, I like how you made your address array dynamic. With your fire function, what would happen if you wanted to remove an address from the front of the array, say index 0? Pretend these letters are addresses [A, B, C, D] and you wanted to remove A. Looks like in your for loop will make index 0 equal to index of 1 so the array would look like [B, B, C, D] and then you pop off D, since pop removes the last index in the array?

@seer Thanks for the share, I like how you made your address array dynamic. With your fire function, what would happen if you wanted to remove an address from the front of the array, say index 0? Pretend these letters are addresses [A, B, C, D] and you wanted to remove A. Looks like in your for loop will make index 0 equal to index of 1 so the array would look like [B, B, C, D] and then you pop off D, since pop removes the last index in the array?

In fact, what it is supposed to do is rewrite almost the entire array so every element is copied to its index - 1, starting from the element which needs to be removed. So in your example it should go from [A,B,C,D] to [B,C,D,D] and then pop off the last element resulting in [B, C, D].

2 Likes

Ahh got it, nice implementation.

pragma solidity 0.7.5;
pragma abicoder v2;

contract MultiSigsContract{
    
    event logMessage(string message);
    
    modifier approversOnly {
        require(
            msg.sender == approver1
            || msg.sender == approver2
            || msg.sender == approver3,
            "Only approver addresses can approve transactions."
        );
        _;
    }
    
    struct Transaction {
        uint id;
        address from;
        address to;
        uint amount;
    }
    
    address approver1;
    address approver2;
    address approver3;
    Transaction[] transactions;
    mapping(uint => address[]) approbations;

    
    // specifying the other 2 approvers, the first being the contract initiator
    constructor(address _approver2, address _approver3){
        approver1 = msg.sender;
        approver2 = _approver2;
        approver3 = _approver3;
    }
    
    // Used by users to send a transaction to an address.
    function SendTo(address _to) public payable {
        
        require(msg.value > 0, "Amount must be at least 1 wei.");
        
        Transaction memory transaction = Transaction(
            transactions.length,
            msg.sender,
            _to,
            msg.value
        );
        transactions.push(transaction);
        
        emit logMessage("Transaction added.");
    }
    
    // Get infos of a specific transacton, pending or executed.
    function GetTransactionInfos(uint _index) public returns(Transaction memory tx, bool isExecuted){
        
        Transaction memory tx = transactions[_index];
        
        return (
            tx,
            TransactionIsExecuted(_index)
        );
    }
    
    // Get a list of pending txs. Could be used by the senders or approvers.
    function GetPendingTransactions() public returns (Transaction[] memory){
        
        Transaction[] memory txs = new Transaction[](transactions.length);
        uint counter = 0;
        for(uint i = 0; i < transactions.length; i++){
            if(TransactionIsExecuted(i) == false){
                txs[counter++] = transactions[i];
            }
        }
        
        return txs;
    }
    
    // Used by approvers to approve a tx. When 2 out of 3 approvers approved, the tx is executed.
    function ApproveTransaction(uint _index) public approversOnly returns(bool isExecuted){
        
        // add to tx approbations
        address[] memory txApprobations = approbations[_index];
        address currentApproverAddress = msg.sender;
        
        if(txApprovedByAddress(txApprobations, currentApproverAddress) == false){
            address[] memory appendedTxApprobations = AppendAddressToTxApprobations(_index, currentApproverAddress);
            approbations[_index] = appendedTxApprobations; // assign
            
            // execute tx if 2/3 approbations threshold is reached. Note : if more that 2 approvers is reached, the tx will NOT be executed again.
            if(appendedTxApprobations.length == 2){
                return ExecuteTransaction(_index);
            } else {
                emit logMessage("Tx approbation threshold not reached or tx already executed.");
                return false;
            }
            
            
        } else /* already approved by this address*/ {
            emit logMessage("Tx already approved.");
            return false;
        }
        
    }
    
    // For debugging purposes.
    function GetBalance() public returns (uint balance){
        return address(this).balance;
    }
    
    // Add an address to the list of address so that we can update the approval status.
    function AppendAddressToTxApprobations(uint _index, address _address) private returns (address[] memory addresses){
        
        // clone existing values
        address[] memory txApprobations = approbations[_index];
        address[] memory arr = new address[](txApprobations.length + 1);
        for(uint i = 0; i < txApprobations.length; i++){
            arr[i] = txApprobations[i];
        }
        
        // append and return
        arr[txApprobations.length] = _address;
        
        return arr;
    }
    
    // Used to check if an address(approver) already approved this tx.
    function txApprovedByAddress(address[] memory txApprobations, address approverAddress) private returns (bool){
        for(uint i = 0; i < txApprobations.length; i++){
            if(txApprobations[i] == approverAddress){
                return true;
            } else {
                return false;
            }
        }
    }
    
    // Checks if the tx is already executed.
    function TransactionIsExecuted(uint _index) private returns(bool isExecuted){
        
        address[] memory txApprovers = approbations[_index]; // contains the addresses that approved this transaction
        if(txApprovers.length > 1){
            return true;
        } else {
            return false; // pending
        }
        
    }
    
    // Sends the tx funds to the destination.
    function ExecuteTransaction(uint _index) private returns(bool isExecuted){
        emit logMessage("Executing tx...");
        Transaction memory tx = transactions[_index];
        payable(tx.to).transfer(tx.amount);
        return true;
    }
    
}
1 Like
pragma solidity 0.7.5;

contract Wallet {
    
    struct Transaction {
        address origin;
        address to;
        uint txId;
        uint approvals;
        bool sent;
        uint amount;
    }
    
    mapping(address => mapping(uint => bool)) approvals;
    mapping(address => uint) public balances;
    
    Transaction[] transactionRequests;
    
    address[] public owners;

    event TransferApproved(uint _id);
    event TransferRequested(address indexed _from, address indexed _to, uint _amount);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    
    modifier onlyOwner() {
        bool isOwner = false;
        for(uint i = 0; i < owners.length ; i++){
            if(isOwner == false){
                isOwner = owners[i] == msg.sender;
            }
        }
        require(isOwner);
        _; // run the function --> line will be replaced with function call
    }
    
    uint limit;
    
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    function deposit(address _recipient, uint _amount) onlyOwner public {
        balances[_recipient] += _amount;
    }
    
    function createTransfer(address _from, address _recipient, uint _amount) public {
        emit TransferRequested(msg.sender, _recipient, _amount);
        transactionRequests.push(
                Transaction(
                    _from,
                    _recipient,
                    transactionRequests.length,
                    0,
                    false,
                    _amount
                    )
            );
    }
    
    function approve(uint _txId) onlyOwner public {
        require(approvals[msg.sender][_txId] == false);
        require(transactionRequests[_txId].sent == false);
        
        approvals[msg.sender][_txId] = true;
        transactionRequests[_txId].approvals++;
        
        emit ApprovalReceived(_txId, transactionRequests[_txId].approvals, msg.sender);
        
        uint requestApprovals = 0;
        for (uint i=0; i< owners.length; i++) {
            if(approvals[owners[i]][_txId] == true) {
                requestApprovals++;
                continue;
            }
        }
        
        if(requestApprovals >= limit) {
            require(balances[transactionRequests[_txId].origin] > transactionRequests[_txId].amount);
            balances[transactionRequests[_txId].origin] -= transactionRequests[_txId].amount;
            balances[transactionRequests[_txId].to] += transactionRequests[_txId].amount;
            
            
            transactionRequests[_txId].sent = true;
            
        
            
            emit TransferApproved(_txId);
        }
    
    }
}```

So this is my atrocious stab at the project. I refused to look ahead and really only use what I learned in the class.
I laid out what I thought should happen as a process map and then created functions to perform the task.s
The contract has hard coded owners that are required to signal that they approve of a transaction request. Anyone can submit a request, and anyone can fund the contract. At least two of the owners must change their state to ā€œapproveā€. Afterwards, anyone can tell the transaction to go ahead.

There any many holes that need to be filled such as resetting the owners state after a transaction has occurred, etc.

After looking at many of the other solutions above I can see this is definitely the ugliest contract. Good. Plenty more best practices to learn.

pragma solidity 0.7.5;
pragma abicoder v2;

contract wallet{
    
    //owners 
    //0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
    //0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
    //0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db
    
   //create owner data format
    struct owner{
        address ownerAddress;
        bool ownerState;
    }
    
    //create owner variables
    owner owner1 = owner(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, false);
    owner owner2 = owner(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2, false);
    owner owner3 = owner(0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db, false);
    
    
    //creates a new type called storedRequest that holds data in the format below
    struct storedRequest{
        address payable recipient;
        uint amount;
    }
    
    // this creates a variable called request with the storedRequest struct format
    storedRequest request;
    
    function txRequest(address payable recipient, uint amount) public{
        request = storedRequest(recipient, amount);
    }
    
    //function viewState() public view returns(bool){return owner1.ownerState;}
    
    function viewRequest() public view returns(storedRequest memory){
        return(request);
    }
    
    // this changes the owners state from accept to deny
    function changeState(bool state) public returns(string memory Owner){
        if (msg.sender == owner1.ownerAddress){
            owner1.ownerState = state;}
        else if (msg.sender == owner2.ownerAddress){
            owner2.ownerState = state;}
        else if (msg.sender == owner3.ownerAddress){
            owner3.ownerState = state;}
        else { return "you are not an owner";}
    }
        
    //adds funds to the contract
    function fundContract() public payable{}
    
    function checkReadySend() public returns(string memory){
        if ( ((owner1.ownerState == true) && (owner2.ownerState == true))||
             ((owner1.ownerState == true) && (owner3.ownerState == true))||
             ((owner3.ownerState == true) && (owner2.ownerState == true))   ){
            request.recipient.transfer(request.amount);
            return "approved";
        }
        else { return "not approved";}
    }
    
    function getBalance() public view returns(uint){
        return address(this).balance;
    }
    
    
    
    
    
} //end
2 Likes

I am trying hard to solve this problem without continuing to next video, my thesis is, each transaction contains multiple approvals; the approvals are basically signatory addresses. I am trying to achieve this by creating 2 separate structs one for Transaction and one for Approval. Once a spending transaction is created, the contract has instance of Transaction as state variable, the signatories simply enter their addresses as approval in a separate function call. The transaction is mapped to the signatory addresses in a state variable which is basically a struct. So my question to you @filip is

  1. How should I make the transaction reviewable? meaning transaction for instance is just a state variable the signatory can not see the details of it while approving it. So I want to make this transaction details visible to reviewer?
  2. Is this a right approach am I missing something?

Following are some code details.

  1. Declare 2 different structs and respective variables as below
    struct Transaction {
    address from;
    address to;
    uint amount;
    }
    struct Approvals {
    Transaction transaction;
    address[] signs;
    }
    Transaction transaction;
    Approvals approval;

  2. create functions 3 separate functions
    a. spend((address _from, address _to, uint _amount) to create transaction.
    b. getApprovals(address _signatory) for the transaction created in spend, once a transaction has enough approvals do transfer
    c. transfer(Transaction memory _transaction) to adjust balance once transaction is approved.

function getApprovals(address _signatory) public {
approval.transaction = transaction;
approval.signs.push(_signatory);
if(approval.signs.length > 2) {
transfer(approval.transaction);
}
}

function spend(address _from, address _to, uint _amount) public {
    transaction = Transaction(_from, _to, _amount);
    emit spendTransaction(transaction);
}

function transfer(Transaction memory _transaction) private {
    balance[_transaction.from] -= _transaction.amount;
    balance[_transaction.to] += _transaction.amount;
}
1 Like

This is what I came up with from just watching the intro. Not sure If Iā€™m doing the right thing?

I have 3 contracts.

1- MultisigGroup.sol :

  • function to add multisig groups with quorum: number of signatures (2 or 3) required for transactions execution.
  • function to confirm group membership
  • small functions to do validations: isGroupCreator, isGroupMember, isGroupConfirmed.

2 - TransactionQueue.sol:

  • function to AddTransaction.
  • function to SignTransaction.
  • function to CancelTransaction.
  • function to ExecuteTransaction

3 - MultisigBank.sol : functions to deposit, withdraw, transfer and getBalance.

The way it works:

Before funds can be deposited, transferred or withdrawn:
A Multisig group needs to be added (the masterkey is the one who creates the group).
then the other 2 keys need to confirm their group membership.

Deposits are direct as they donā€™t need confirmation. Transfers and withdraws are added to a queue and processed once the other key(s) sign the transaction or any of the 3 keys cancel the transaction.

The availability of funds are verified twice, before adding a transaction and before executing a transaction. Transactions are executed by the order of first signed first processed. Therefore a second transaction could be executed before a 1st and not leave enough funds for the first transaction to go through once signed.

The code

ā€” MultisigGroup ā€”

pragma solidity 0.7.5;


contract MultisigGroup {
    
    struct Group {
        uint quorum;
        bool key2Confirmed;
        bool key3Confirmed;
        address masterKey;
        address key2;
        address key3;
    }
    
    // masterKey => Group
    mapping(address => Group) groups;
    
    function createMultisigGroup(uint quorum, address key2, address key3) public {
        require(groups[msg.sender].masterKey != msg.sender, 
        "Group for masterKey already exist");
        require(quorum == 2 || quorum == 3, "The quorum must be 2 or 3");
        require(msg.sender != key2 && msg.sender != key3 && key2 != key3, 
        "Group can't contain duplicate keys");
        groups[msg.sender] = Group(quorum, false, false, msg.sender, key2, key3);
    }

    
    function isGroupConfirmed(address _masterKey) view internal returns (bool) {
        if( groups[_masterKey].key2Confirmed == true && groups[_masterKey].key3Confirmed == true) {
            return true;
        } else {
            return false;
        }
    }
    
    function isGroupMember(address _masterKey) view internal returns (bool) {
        
        if(msg.sender == groups[_masterKey].masterKey || msg.sender == groups[_masterKey].key2 || msg.sender == groups[_masterKey].key3 ) {
            return true;
        } else {
            return false;
        }
        
    }
    
    function isGroupCreator(address _masterKey) view internal returns(bool) {
        if(msg.sender == groups[_masterKey].masterKey) {
            return true;
        } else {
            return false; 
        }
    }
    
    function confirmGroupMembership(address _masterKey)  public returns(string memory) {
        require(groups[_masterKey].masterKey == _masterKey, "Group does not exist");
        require(isGroupMember(_masterKey), "Not member of this group.");
        require(!isGroupConfirmed(_masterKey), "Group total membership already Confirmed.");
        require(!isGroupCreator(_masterKey), "Creator of the group doesn't need to confirm his membership");
        
        if(groups[_masterKey].key2 == msg.sender && groups[_masterKey].key2Confirmed == false ) {
            groups[_masterKey].key2Confirmed = true;
            return "Group Membership Confirmed";
        } else if (groups[_masterKey].key3 == msg.sender && groups[_masterKey].key3Confirmed == false) {
            groups[_masterKey].key3Confirmed = true;
            return "Group Membership Confirmed";
        }
        
        return "Group Membership was already Confirmed";
    }
    
}

ā€” TransactionQueue ā€”

pragma solidity 0.7.5;

import "./MultisigGroup.sol";

contract TransactionQueue is MultisigGroup{
    
    // possible transaction status
    uint8 constant WAITTING = 0;
    uint8 constant COMPLETED = 1;
    uint8 constant CANCELED = 2;
    
    //possible transaction types
    uint8 constant WITHDRAW = 0;
    uint8 constant TRANSFER = 1;

    struct Transaction {
        uint transactionID;
        address masterKey;
        uint amount;
        uint8 transactionStatus;
        bool isKey1Signed;
        bool isKey2Signed;
        bool isKey3Signed;
        uint8 transactionType;
        address payable payTo;
    }
    
    Transaction[]  transactions; 
    
    event transactionAdded(uint indexed transactionID, uint amount, address indexed payTo);
    
    mapping(address => uint) balance;

    function addTransaction(address _masterKey, uint _amount, uint8 _transactionType,
    address payable _payTo) internal returns(uint){
        
        // Only allow to add a transaction if member of the masterKey group
        // and all keys must be confirmed in the group
        require(isGroupMember(_masterKey), "Not member of this group.");
        require(isGroupConfirmed(_masterKey), "All keys must be confirmed before doing any transactions.");
       
        // only add a transaction to the queue if the  balance is >= amount
        require(balance[_masterKey] >= _amount, "Non-sufficient Funds");
        
        // initialize the signing of the transaction.
        bool _isKey1Signed = false;
        bool _isKey2Signed = false;
        bool _isKey3Signed = false;
        if(msg.sender == groups[_masterKey].masterKey) {
            _isKey1Signed = true;   
        } else if (msg.sender == groups[_masterKey].key2) {
            _isKey2Signed = true;
        } else if (msg.sender == groups[_masterKey].key3) {
            _isKey3Signed = true;
        }
        
        transactions.push(Transaction(
            transactions.length, _masterKey, _amount, WAITTING, 
            _isKey1Signed, _isKey2Signed, _isKey3Signed, _transactionType,  _payTo ));
    
        emit transactionAdded(transactions.length, _amount, _payTo);
        return transactions.length;
    }
    
    function cancelTransaction(uint _transactionID) public {
        require(_transactionID < transactions.length, "Transaction does not exist");
        require(isGroupMember(transactions[_transactionID].masterKey), "Not member of the transactions group.");
        require(transactions[_transactionID].transactionStatus == WAITTING, "Transaction already executed or canceled");
        
        transactions[_transactionID].transactionStatus = CANCELED;
        
    }
    
    function signTransaction(uint _transactionID) public {
        
        require(_transactionID < transactions.length, "Transaction does not exist");
        require(isGroupMember(transactions[_transactionID].masterKey), "Not member of the transactions group.");
        require(transactions[_transactionID].transactionStatus == WAITTING, "Transaction already executed or canceled");
        
        if(msg.sender == groups[transactions[_transactionID].masterKey].key3) {
            transactions[_transactionID].isKey3Signed = true;   
        } else if (msg.sender == groups[transactions[_transactionID].masterKey].key2) {
            transactions[_transactionID].isKey2Signed = true;
        } else if (msg.sender == groups[transactions[_transactionID].masterKey].masterKey) {
            transactions[_transactionID].isKey1Signed = true;
        }


        //Count the quorum 
        uint8 quorumCount = 0;
        if(transactions[_transactionID].isKey1Signed) { quorumCount++; }
        if(transactions[_transactionID].isKey2Signed) { quorumCount++; }
        if(transactions[_transactionID].isKey3Signed) { quorumCount++; }
        // if quorum is met execute the transactions
        if(quorumCount >= groups[transactions[_transactionID].masterKey].quorum) {
            executeTransaction(_transactionID);
        }
    }
    
    function executeTransaction(uint _transactionID) internal returns (uint){
        
        address _from = transactions[_transactionID].masterKey;
        uint _amount = transactions[_transactionID].amount;
        address payable _payTo = transactions[_transactionID].payTo;
        uint8 _transactionType = transactions[_transactionID].transactionType;
        
        require(balance[_from] >= _amount, "Balance not sufficient");
        
        assert(_transactionType == WITHDRAW || _transactionType == TRANSFER);
        
        if(_transactionType == WITHDRAW) {
            balance[_from] -= _amount;
            _payTo.transfer(_amount);
            transactions[_transactionID].transactionStatus = COMPLETED;
            return balance[_from];
        } else { //TRANSFER
            balance[_from] -= _amount;
            balance[_payTo] += _amount;
            transactions[_transactionID].transactionStatus = COMPLETED;
            return balance[_from];
        }
        
    }

}

ā€” MulisigBank ā€”

pragma solidity 0.7.5;
    
import "./TransactionQueue.sol";


contract MultisigBank is TransactionQueue  {
    
    address payable internal owner;
    
    constructor(){
        owner = msg.sender;
    }
    
    event depositDone(uint amount, address indexed depositedTo); 
    
   
    function deposit(address masterKey) public payable returns (uint){  
        //The group must be confirmed before receiving any transactions
        require(isGroupConfirmed(masterKey), "Group membership must be confirmed first.");
        balance[masterKey] += msg.value;
        emit depositDone(msg.value, masterKey);
        return balance[masterKey];
        
    } 
    
    function withdraw(address _masterKey, uint _amount) public {
        
        addTransaction(_masterKey, _amount, WITHDRAW, msg.sender); 
    
    }
    
    function getBalance(address _masterKey) public view returns (uint){
    return balance[_masterKey];
    } 
    
    function transfer(address _masterKey, uint _amount, address payable _payTo) public {
    
        require(_masterKey != _payTo, "The sender and recipient cant be the same");
        addTransaction(_masterKey, _amount, TRANSFER, _payTo);
        
    } 
    
    
}   

A pretty basic implementation of the multi sig wallet. It has no fancy features like a member address manager or a transaction request list explorer.

It only processes the request and votes of three (hardcoded) members. Once a transaction request is made and 2 out of 3 members vote for that request, the transaction is executed. Transaction requests are stored in an array and once a transaction is executed (voted by the majority) its state changes to ā€œexecutedā€. You can not request a transaction if there is another one identical (same amount and same address).

pragma solidity 0.7.5;

contract MultiSigWallet {

address[3] public owners;
uint8 approvalLimit;
transfer[] transfers;
uint transferCounter;
address owner1;
address owner2;
address owner3;

struct transfer{
   uint transferId;
   address payable receiver;
   uint amount;
   bool approvalOwner1;
   bool approvalOwner2;
   bool approvalOwner3;
   bool transactionExecuted;
}

event transactionRequested(uint, address, uint);
event transactionExecuted(uint, address, uint);

constructor(){
    owner1 = 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB;
    owner2 = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;
    owner3 = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
    owners = [owner1,owner2,owner3];
    approvalLimit = 2;
    transferCounter = 0;
}

//Anyone can deposit in this contract
function deposit() public payable returns(uint){
    return address(this).balance;
}

function requestTransaction(address payable receiver, uint amount) public returns(bool){
    //Check that the request comes from a registered address
    bool addressIsOwner = false;
    for (uint i = 0; i < owners.length; i++){
        if (msg.sender == owners[i]){
            addressIsOwner = true;
        }
    }
    require(addressIsOwner == true, "Your address is not registered");
    require(address(this).balance >= amount, "Not enough funds");
    
    //Check that there is no other equal request (same amount to the same address)
    if (transfers.length > 0){
        bool transactionAlreadyRequested = false;
        for (uint i = 0; i < transfers.length; i++){
            if (receiver == transfers[i].receiver && amount == transfers[i].amount && transfers[i].transactionExecuted == false){
                transactionAlreadyRequested = true;
            }
    }
        require(transactionAlreadyRequested == false, "An equal trasnaction already exists");
    }
    
    //If everything is ok then proceed to add the request to the request list
    emit transactionRequested(transferCounter, receiver, amount);
    transfers.push(transfer(transferCounter, receiver, amount, false, false, false, false));
    transferCounter++;
    
    return true;
}

function voteTransaction(uint id, bool vote) public returns(bool){
    //Check that the request comes from a registered address
    bool addressIsOwner = false;
    for (uint i = 0; i < owners.length; i++){
        if (msg.sender == owners[i]){
            addressIsOwner = true;
        }
    }
    require(addressIsOwner == true, "Your address is not registered");
    //Validate that the transaction is on the request list
    require(id <= transfers.length, "Transaction does not exist");
    //Validate that the transaction has not been executed yet
    require(transfers[id].transactionExecuted == false, "Transaction has already been executed");
    //If everything is ok, update the vote status of the corresponding requester
    if (msg.sender == owner1){
        transfers[id].approvalOwner1 = vote;
    }
    if (msg.sender == owner2){
        transfers[id].approvalOwner2 = vote;
    }
    if (msg.sender == owner3){
        transfers[id].approvalOwner3 = vote;
    }
    //Add all the votes of this request, if approved then execute the requested transaction
    uint8 approvalOwner1 = transfers[id].approvalOwner1 ? 1 : 0;
    uint8 approvalOwner2 = transfers[id].approvalOwner2 ? 1 : 0;
    uint8 approvalOwner3 = transfers[id].approvalOwner3 ? 1 : 0;
    
    if(approvalOwner1 + approvalOwner2 + approvalOwner3 >= approvalLimit){
        executeTransaction(id);
    }
    return true;
    
}

function executeTransaction(uint id) private returns(bool){
    require(address(this).balance >= transfers[id].amount, "Not enough funds");
    transfers[id].receiver.transfer(transfers[id].amount);
    transfers[id].transactionExecuted = true;
    emit transactionExecuted(id, transfers[id].receiver, transfers[id].amount);
    
    return true;
}

}

1 Like

My Solution - created 2 structs, first Transaction with from, to and amount, approvalStatus field and second Approve with transactionId and array of signature addresses.

cruces

  1. Spend transaction - each time a new transaction created with (from, to, amount, approvalStatus) create a approval with (transactionId, emptyAddress[])
  2. Signatories can find all the unapproved transactions by function findUnApprovedTransactions()
  3. Signatories can approve transaction in getApprovals() method, which in the same context search the total number of approvals each transaction has, if it determines a transaction has enough approvals it then call transfer method.
  4. transfer(_transaction) method completes transfer and emits the transfer details.

Please review.

pragma solidity 0.7.5;
pragma abicoder v2;
contract Signatary {
address[] signatories;
constructor() {
signatories.push(msg.sender);
}
struct Transaction {
address from;
address to;
uint amount;
bool approved;
}
struct Approval {
uint transactionId;
address[] signatures;
}
Transaction[] transactions;
Approval[] approvals;
mapping(address => uint) accountBalance;
}

pragma solidity 0.7.5;
pragma abicoder v2;
import ā€œ./Signatary.solā€;

contract Wallet is Signatary {
uint MINIMUM_APPROVAL_SIGNS = 2;
event unapprovedSpendTransactionCreatedWithApprovalId(uint);
event unApprovedTransactions(uint unApprovedTransactionId);
event transferDone(address from, address to, uint amount)
;
function deposit(address account, uint amount) public payable returns(uint) {
return accountBalance[account] += amount;
}

function getApprovals(uint _approvalId) public {
    Transaction memory transaction = transactions[approvals[_approvalId].transactionId];
    approvals[_approvalId].signatures.push(msg.sender);
    if(approvals[_approvalId].signatures.length >= MINIMUM_APPROVAL_SIGNS) {
        transaction.approved = true;
        transfer(transaction);
    }
}

function spend(address _from, address _to, uint _amount) public {
    transactions.push(Transaction(_from, _to, _amount, false));
    address[] memory noApprovals;
    approvals.push(Approval((transactions.length -1), noApprovals));
    emit unapprovedSpendTransactionCreatedWithApprovalId(approvals.length);
}

function transfer(Transaction memory _transaction) private {
    accountBalance[_transaction.from] -= _transaction.amount;
    accountBalance[_transaction.to] += _transaction.amount;
    emit transferDone(_transaction.from, _transaction.to, _transaction.amount);
}

function getBalance(address _address) public view returns(uint) {
    return accountBalance[_address];
}

function findUnApprovedTransactions() public {
    for(uint i = 0; i < transactions.length; i++) {
        Transaction memory transaction = transactions[i];
        if(transaction.approved == false) {
            emit unApprovedTransactions(i);
        }
    }
}

}

1 Like

Hi, so this may be a bit long.

  1. The contract creator will need to input a number of addresses for the constructor to know who the owners are.
  2. Majority of owners need to approve an address before ā€œtransferā€ can be invoked.
  3. Approval will reset after 1 transaction.
  4. I guess this can be improved with unlimited transactions after approval easily.
  5. Need to include amount into approval struct.

MultiOwnable.sol

contract MultiOwnable {
address public contractCreator;
address[] public ownersArray;
uint public addressCount;

constructor(address[] memory _addressArray) {
    contractCreator = msg.sender;
    for (uint i = 0; i < _addressArray.length; i++){
        ownersArray.push(_addressArray[i]);
    }
    addressCount = ownersArray.length;
}

function checkIsOwner (address _checkIsOwner) view internal returns (bool) {
    bool result;
    
    for (uint i = 0; i < addressCount; i++){
        if (_checkIsOwner == ownersArray[i]) {
            result = true;
            break;
        }
        else{
            result = false;
        }
    }
    return result;
}

modifier onlyOwners {
    require (checkIsOwner(msg.sender), "Not creator!");
    _; // run the function after modifier
}

}

MultiSig.sol

pragma solidity 0.7.6;
pragma abicoder v2;

import ā€œ./MultiOwnable.solā€;

contract MultiSig is MultiOwnable {

mapping(address => ApprovedAddress) public approvedAddress;

uint public signaturesNeeded;

struct ApprovedAddress{
    mapping(address => bool) hasSigned; 
    uint numberOfSignatures;
    bool approved;
}

constructor (address[] memory _addressArray) MultiOwnable (_addressArray) {
    signaturesNeeded = (_addressArray.length / 2) + 1;
}

event depositDone (address sender, uint amount);
event withdrawDone (address receipientAddress, uint amount);
event enoughSignatures (address approvedAddress, bool approved);
event approvalReset (address);

function deposit() public payable returns (uint) {
    emit depositDone(msg.sender,msg.value);
    return address(this).balance;
}

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

function getMySignature (address _approvedAddress) public view returns (bool){
    return approvedAddress[_approvedAddress].hasSigned[msg.sender];
}

function getSignatureOf (address _approvedAddress, address _approver) public view returns (bool) {
    return approvedAddress[_approvedAddress].hasSigned[_approver];
}

function sign(address _approvedAddress) public onlyOwners returns (bool) {
approvedAddress[_approvedAddress].hasSigned[msg.sender] = true;
approvedAddress[_approvedAddress].numberOfSignatures++;
return checkEnoughSignatures(_approvedAddress);
}

function transfer (address payable recipient, uint amount) public payable onlyOwners {
   require (approvedAddress[recipient].approved);
   require (address(this).balance >= amount);
   
   uint previousBalance = address(this).balance;
   
   recipient.transfer(amount);
   emit withdrawDone (recipient,amount);
   
   assert(address(this).balance == (previousBalance - amount));
   resetApproval(recipient);
}

function checkEnoughSignatures (address _approvedAddress) private returns (bool){
    uint _hasSigned = 0;
    
    for (uint i = 0; i < ownersArray.length; i++){
        if (approvedAddress[_approvedAddress].hasSigned[ownersArray[i]]){
            _hasSigned++;
        }
    }
    
    if (_hasSigned >= signaturesNeeded){
        approvedAddress[_approvedAddress].approved = true;
        emit enoughSignatures (_approvedAddress, approvedAddress[_approvedAddress].approved);
    }
    
    return approvedAddress[_approvedAddress].approved;
}

function resetApproval (address _approvedAddress) onlyOwners public {
    for (uint i = 0; i < ownersArray.length; i++){
        approvedAddress[_approvedAddress].hasSigned[ownersArray[i]]=false;
    }
    approvedAddress[_approvedAddress].approved=false;
    approvedAddress[_approvedAddress].numberOfSignatures=0;
    
    assert(approvedAddress[_approvedAddress].approved == false 
    && approvedAddress[_approvedAddress].numberOfSignatures == 0);
    
    emit approvalReset(_approvedAddress);
}

}

I just donā€™t totally understand the assignment. All Philip said was create a multisig wallet. Are there other details Iā€™m missing?

I have the same question. Filip said you can ā€œcheck out the requirements belowā€, but they arenā€™t there.

From the lesson video, it just sounds like you have to get 2 out of 3 available signatures before you can transfer funds. Is that the complete assignment?