Project - Multisig Wallet

Thanks Carlos,
Sorry for the delay, I was full on my new startup.

I understand your points.
Regarding your first point, I guess I see how to fix it but I’m not sure.
Here is the new version of my contract with some other extra improvements, I’m keeping track of the totalRequested as a state variable so that I don’t have to calculate it every time. Does it fix the problem or is it introducing new concerns?

pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

abstract contract Ownable {
    
    uint internal numberOfApprovals;
    
    mapping (address => bool) owners;
    
    modifier onwersAccessOnly{
        require(owners[msg.sender],'Accessible by owners only');
        _;
    }
    
}

//If it was in another file Owner.sol at the same level
//import "./Ownable.sol";

abstract contract Destroyable is Ownable {
    function destroy() public onwersAccessOnly {
        selfdestruct(payable(msg.sender));
    } 
}


contract Multisig is Destroyable{
    
    struct TransferRequest {
        uint id;
        address recipient;
        address[] acceptedBy;
        uint amount;
        //bool paid;
    }

    mapping (uint => TransferRequest) transferRequests;
    uint totalRequested;
    uint lastId;
    
    
    event moneyDeposited(address _owner,uint _amount);
    //transfer request id,requester,transfer recipient, amount
    event transferRequested(uint _transferRequestId,address _requester,address _recipient,uint _amount);
     //transfer request id,transfer recipient, amount
    event transferAccepted(uint __transferRequestId,address _requester,uint _amount);

    //example ["0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"],2
     constructor(address[] memory _owners,uint _numberOfApprovals ) {
        require(_numberOfApprovals < _owners.length && _numberOfApprovals > 0 , 'Invalid number of approvals');
        
        for(uint i = 0 ; i < _owners.length ; i++){
            owners[_owners[i]] = true;
        }
        numberOfApprovals=_numberOfApprovals;
    }
    
    function deposit () public onwersAccessOnly payable returns (uint){
        emit moneyDeposited(msg.sender,msg.value);
        return address(this).balance;
    }
    
    //example "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",2000000000000000000
    //example "0x583031D1113aD414F02576BD6afaBfb302140225",4000000000000000000
    //amount int wei
    function requestTransfer (address _recipient, uint _amount) public onwersAccessOnly returns (uint) {
        require(_amount!=0,"Cannot request a transfer with 0 amount");
        require(address(this).balance>=_amount,"Not enough money in the wallet to accept the transfer request");
        require(address(this).balance-totalRequested>=_amount,"Too much money already pledged to accept the transfer request");
       /* uint totalRequested=0;
        uint size=0;
        //to check if this key exist
        while (transferRequests[size].amount!=0){
            if (!transferRequests[size].paid){
                totalRequested+=transferRequests[size].amount;
            }
            size++;
        }*/
       
        TransferRequest memory _transferRequest=TransferRequest(lastId,_recipient,new address[](0),_amount/*,false*/);
        transferRequests[lastId]=_transferRequest;
        transferRequests[lastId].acceptedBy.push(msg.sender);
        
        emit transferRequested(_transferRequest.id,msg.sender,_transferRequest.recipient,_transferRequest.amount);
        _attemptApproval(lastId);
        totalRequested+=_amount;
        lastId++;
        return _transferRequest.id;
    }
    
    //recipient, amount, #approvals
    function getRequestTransferDetails(uint _transfertRequestId) public view onwersAccessOnly returns(TransferRequest memory) {
        _transfertRequestExists;
        return transferRequests[_transfertRequestId];
    }
    
    function getRequiredNumberOfApprovals() public view onwersAccessOnly returns(uint) {
        return numberOfApprovals;
    }
    
    function approve(uint _transfertRequestId) public onwersAccessOnly {
        _transfertRequestExists;
        bool alreadyApproved;
        for (uint i=0;i< transferRequests[_transfertRequestId].acceptedBy.length;i++){
            if (transferRequests[_transfertRequestId].acceptedBy[i]==msg.sender){
                alreadyApproved=true;
                break;
            }
        }
        if (!alreadyApproved){
            transferRequests[_transfertRequestId].acceptedBy.push(msg.sender);
            _attemptApproval(_transfertRequestId);
        }
    }
    
    function _attemptApproval (uint _transfertRequestId) private {
        require(_transfertRequestExists(_transfertRequestId), "operator query for nonexistent transaction");
        TransferRequest memory _transferRequest=transferRequests[_transfertRequestId];
        if ( _transferRequest.acceptedBy.length==numberOfApprovals){
            _acceptTransfer(_transferRequest.id);
        }
    }
    
    function _acceptTransfer (uint _transfertRequestId) private {
        require(_transfertRequestExists(_transfertRequestId), "operator query for nonexistent transaction");
        TransferRequest memory _transferRequest=transferRequests[_transfertRequestId];
        require(address(this).balance>=_transferRequest.amount,"Not enough money in the wallet to accept the transfer");
        payable(_transferRequest.recipient).transfer(_transferRequest.amount);
        totalRequested-=_transferRequest.amount;
        emit transferAccepted(_transferRequest.id,_transferRequest.recipient,_transferRequest.amount);
        //transferRequests[_transfertRequestId].paid=true;
        delete transferRequests[_transfertRequestId];
    }
    
    function _transfertRequestExists (uint _transfertRequestId) private view returns(bool) {
        //way of checking if key exists
        //require(transferRequests[_transfertRequestId].amount>0);
        return transferRequests[_transfertRequestId].recipient != address(0);
    }
    
    /*
    To know the contract total deposited amount 
    */
    function getWalletBalance() public view returns(uint256) {
        return address(this).balance;
    }
    
}```
1 Like

Came up with the contract structure and variables after watching first video. Got stuck with the mapping of requests and approvals. Watched the second and here is the code:

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    
    address[] public owners;
    
    uint requiredApprovals;
    uint numberOfOwners;
    uint balance = 0;
    
    struct Transfer{
        uint txid;
        address payable to;
        uint amount;
        uint approvals;
        bool sent;
    }
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approval;
    
    
    modifier onlyOwners() {
        bool isOwner = false;
        for(uint i=0;i<owners.length;i++){
            if(msg.sender == owners[i]){
                isOwner = true;
            }
        }
        
        require(isOwner == true);
        _;
    }
    
    constructor(address[] memory _owners , uint _requiredApprovals) {
        owners = _owners;
        requiredApprovals = _requiredApprovals;
    }

    function deposit(uint _amount) public payable returns(uint){
        balance += _amount;
        return balance;
    }
    
    function transferRequest(address payable _to, uint _amount) onlyOwners public{
        require(_amount < balance,'Hey! Seems you do not have the required balance in the account.');
        transferRequests.push(Transfer(transferRequests.length,_to,_amount,0,false));
    } 
    
    function getTransferRequests() public view returns(Transfer[] memory) {
        return transferRequests;
    }
    
    
    function approve(uint _txId) public onlyOwners {
        require(transferRequests[_txId].sent == false);
        require(approval[msg.sender][_txId] == false);
        
        approval[msg.sender][_txId] = true;
        transferRequests[_txId].approvals++;
        
        if(transferRequests[_txId].approvals >= requiredApprovals){
            transferRequests[_txId].sent = true;
            balance -= transferRequests[_txId].amount;
            transferRequests[_txId].to.transfer(transferRequests[_txId].amount);
        }
    }
    
     
}
3 Likes

Hi all, I have a question regarding pushing addresses in an array.
i try the exercise first by inserting 3 fixed owners in the constructor.

address[] owners;
 constructor(uint _minSignatures, address owner1, address owner2, address owner3){
        minSignatures = _minSignatures;
        owners.push(owner1);
        owners.push(owner2);
        owners.push(owner3);
    }

I tried to push all 3 owners in the array as such.

owners.push(owner1, owner2, owner3);

but I get a syntax error. i tried all kind of formats and also googled it but cant seem to get the right syntax to get it in one line so I wroe it as above.

any help would be much appreciated

1 Like

if your using the constructor to predefine owners in the array then just pass in the owners array.

address[]  public owners;

constructor (address[] memory _owners, uint _minSignatures) {
    owners = _owners;
    minSignatures = _minSignatures
}

before you deploy in remix you will have to input the addresses in the form of [“add1”, “add2”, “add3”] in a little input box that will appear beside the deploy button in the deployment tab in remix.

But did you read the text underneath the video? He gave out requirements of the project there

1 Like

Here is my code:

pragma solidity 0.7.5;

contract Wallet {
    uint approvalsNeeded;
    mapping(address => bool) owners;
    struct Transaction {
        address[] approvedBy;
        uint amount;
        address payable to;
    }
    Transaction[] transactions;
    
    constructor() {
        owners[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = true;
        owners[0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2] = true;
        owners[0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c] = true;
        approvalsNeeded = 2;
    }
    
    function deposit() payable public {}
    
    function getBalance() public view returns(uint) {
        return address(this).balance;
    }
    
    function transfer(uint amount, address payable to) public returns(uint transferId) {
        require(address(this).balance >= amount);
        require(owners[msg.sender] == true, "You are not one of the owners of this contract!");
        address[] memory empty;
        transactions.push(Transaction(empty, amount, to));
        return transactions.length - 1;
    }
    
    function approveTransaction(uint transferIndex) public {
        require(owners[msg.sender] == true, "You are not one of the owners of this contract!");
        Transaction storage transaction = transactions[transferIndex];
        for (uint i = 0; i < transaction.approvedBy.length; i++) {
            require(transaction.approvedBy[i] != msg.sender, "You have already approved this transaction!");
        }
        transaction.approvedBy.push(msg.sender);
        if (transaction.approvedBy.length >= approvalsNeeded) {
            transaction.to.transfer(transaction.amount);
        }
    }
}

Please leave feedback

1 Like

Here’s my solution. Note that it is hard-coded for 2 of 3 signatures and the first signature is the initial requestor.

pragma solidity 0.8.4;

import "./Destroyable.sol";

contract Wallet is Destroyable {
    
    address[3] private owners; // My first 3 addresses are the owners
    uint minsigners = 2; // 2 approvals required

    event DepositDone(uint amount, address indexed depositee);
    event SpendRequested(uint amount, address recipient, address indexed requestor, uint id);
    event TransferDone(uint amount, address indexed transferFrom, address indexed transferTo);

    Transaction[] SpendRequests;

    struct Transaction {
        uint id;                    // id (index) of this transaction
        address requestor;          // Address requesting spend transaction
        uint amount;                // Amount requested to spend
        address payable recipient;  // Recipient of transaction
        address[] approvers;        // Approvers of this spend request
        bool hasBeenSent;           // Approved and sent
    }
    
    mapping(address => mapping(uint => bool)) approvals; // not used
   
    modifier onlyOwners {
        require(msg.sender == owners[0] || msg.sender == owners[1] || msg.sender == owners[2]);
        _; // run the function
    }
    
    constructor() {
        owners[0] = msg.sender;
        owners[1] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
        owners[2] = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
    }

    function newSpendRequest(uint amount, address recipient) public onlyOwners {
        require(amount > 0, "Must specify positive amount");
        
        // Add transfer request to array
        Transaction memory tran;
        tran.id = SpendRequests.length; // record transaction id (index)?
        tran.requestor = msg.sender;
        tran.amount = amount;
        tran.recipient = payable (recipient);
        tran.hasBeenSent = false;
        SpendRequests.push(tran);
        SpendRequests[tran.id].approvers.push(msg.sender); // Assume requestor is first approver
        emit SpendRequested(amount, recipient, msg.sender, tran.id);
    }

    function approve(uint _id) public onlyOwners {
        require(_id < SpendRequests.length, "Invalid request id");
        require(SpendRequests[_id].hasBeenSent == false,  "Transfer already sent");
        for(uint i = 0; i < SpendRequests[_id].approvers.length; i++) {
            require(SpendRequests[_id].approvers[i] != msg.sender, "Can't approve your own request");
        }
        // Don't allow final approval if insufficient funds
        require( (SpendRequests[_id].approvers.length < (minsigners - 1)) 
                || (address (this).balance >= SpendRequests[_id].amount), "Insufficient Balance");

        SpendRequests[_id].approvers.push(msg.sender);
 
        // If all signatures met, do the transfer
        if(SpendRequests[_id].approvers.length >= minsigners) {
            SpendRequests[_id].hasBeenSent = true; // Don't allow recursive calls
            SpendRequests[_id].recipient.transfer(SpendRequests[_id].amount);
            emit TransferDone(SpendRequests[_id].amount,address (this), SpendRequests[_id].recipient);
        }
    }
    
	function deposit() public payable {
        emit DepositDone(msg.value, msg.sender);
	}
	
	function getBalance() public view onlyOwners returns (uint) {
	    return address (this).balance;
	}
	
    // Return All spend requests
    function getSpendRequests() public view returns (Transaction[] memory){
        return SpendRequests;
    }
    
 }
1 Like

@mcgrane5 so noone is commenting on my solution :slight_smile: ? is it perfect?

1 Like

hey @1984. Sorry i dont have time to comment on all of them. I see hat you hardcode in the individual addresses. see my latest post above. instead of passing in individual addresses into the constructor, pass in an array of addresses. It will make you code look clesner your validateTransaction as you wont have to write a massive if statement like

if((msg.sender==owneraddress1||msg.sender==owneraddress2||msg.sender==owneraddress3)
    &&(validationList[msg.sender][_transactionNumber]!=true&&validation == true)){

A lot of your functions are not secure. You need to make an onlyOwners modifier in order to restrict some functions to ownly the wallet owners.For example your deposit function is currently

function deposit() public payable {}

it should be

function deposit public payable onlyOwners {
    //need to deposit an amount greater than 0
    require(msg.value >0);
}

also your createTransfer function definitely needs this ownlyOwners modifier. Without it any address can create a transfer request even people not in the owners array which should not be possible. so when you code the ownly owners modifier include it in th efunction header

function createTransaction(address to, uint amount) public onlyOwners {.......

same with validate trasaction

function validateTransaction(bool validation, uint _transactionNumber) public onlyOwners {.....

other than this all looks very pretty good (perhaps add a balance mapping also). Great attempt well done.

Evan

2 Likes

thx @mcgrane5, will check that all

Yes thank you :slight_smile: I saw the requirement but I was experimenting outside the box.
when you add f.e. 3 address variables in the constructor header, once deployed the user will have 3 boxes to insert the 3 addresses which is pretty neat IMO and more user friendly.
Like this:
image

But then I came across this anoying push. In javascript I’m allowed to push several variables in an array with one line of code like owners.push(owner1, owner2, owner3); (this will give a syntax error)

JS example:
image

Just wondering if there is a way to do it in solidity.
instead I did it like that:

        owners.push(owner1);
        owners.push(owner2);
        owners.push(owner3);
1 Like

Solution for Multsig Wallet project

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.6 <0.9.0;

contract Wallet {
    constructor () {
        owner = msg.sender;
        signatories[owner] = (Signatory(owner, block.timestamp, true));
    }
    
    /// State variables
    uint transferId;
    uint depositId;
    uint public approvalLimit;
    uint public walletBalance;
    address public owner;
    
    /// Events
    event Deposited(uint amount, address depositedBy, uint walletBalance);
    event TransferRequested(uint id, address requestedBy, address to, uint amount);
    event TransferSent(address sentBy, address to, uint amount);
    
    /// Mappings
    mapping(address => Signatory) public signatories;
    mapping (address => mapping(uint => Deposit)) public deposits;
    mapping(uint => Transfer) public transfers;
    mapping (address => mapping(uint => bool)) public approvals;
    
    /// Structs
    struct Signatory {
        address signatoryAddress;
        uint createdAt;
        bool isOwner;
    }
    
    struct Deposit {
        address depositedBy;
        uint amount;
        uint depositedAt;
    }
    
    struct Transfer {
        uint id;
        address beneficiary;
        uint amount;
        uint approvals;
        bool sent;
        address creadtedBy;
        uint createdAt;
        address sentBy;
        uint sentAt;
    }
    

    /// Modifiers
    modifier onlyOwner {
        require(msg.sender == owner, "Only contract owner is permitted to use this!");
        _; 
    }
    
    modifier onlySignatories {
        require(msg.sender == signatories[msg.sender].signatoryAddress, "Only approved signatories are permitted to use this!");
        _; 
    }

    /// Functions
    function setApprovalLimit(
        uint _approvalLimit
    ) public onlyOwner {
        approvalLimit = _approvalLimit;
    }
    
    function addSignatory(
        address _signatory
    ) public onlyOwner {
        signatories[_signatory] = Signatory(_signatory, block.timestamp, false);
    }
    
    function deposit() public payable onlySignatories returns (uint, address, uint) {
        deposits[msg.sender][depositId] = Deposit(msg.sender, msg.value, block.timestamp);
        walletBalance += msg.value;
        depositId = depositId + 1;
        
        emit Deposited(msg.value, msg.sender, walletBalance);
        
        return (msg.value, msg.sender, walletBalance);
    }
    
    function createTransferRequest(
        address _transferTo,
        uint _amount
    ) public onlySignatories returns (uint, address, address, uint, uint, bool) {
        uint _approvals = 0;
        bool _sent = false;
        uint _transferId = transferId;
        
        require(_amount > 0, "Transfer amount must be greater than 0 eth.");
        
        transfers[_transferId] = Transfer(
            _transferId,
            _transferTo,
            _amount,
            _approvals,
            _sent,
            msg.sender,
            block.timestamp,
            address(0),
            0
        );
        transferId = _transferId + 1;
        
        emit TransferRequested(_transferId, msg.sender, _transferTo, _amount);
        
        return (_transferId, msg.sender, _transferTo, _amount, _approvals, _sent);
    }
    
    function approveTransfer(uint _transferId) public onlySignatories returns (uint) {
        bool transferExists = getTransferExists(_transferId);

        require(transferExists, "No transfer can be found for the provided transfer id.");
        require(!approvals[msg.sender][_transferId], "You have already approved this transfer.");

        approvals[msg.sender][_transferId] = true;
        transfers[_transferId].approvals = transfers[_transferId].approvals + 1;

        return _transferId;
    }
    
    function getTransferExists(uint _transferId) public view returns (bool) {
        return transfers[_transferId].id == _transferId;
    }
    
    function getTransfer(uint _transferId) internal view returns (Transfer memory) {
        return transfers[_transferId];
    }
    
    function transfer(uint _transferId) public payable onlySignatories {
        bool transferExists = getTransferExists(_transferId);
        
        require(transferExists, "No transfer can be found for the provided transfer id.");
        require(transfers[_transferId].approvals >= 2, "A transfer needs at least 2 approvals before it can be sent.");
        require(transfers[_transferId].amount <= walletBalance, "There are insufficient funds in the wallet to make this transfer.");
        require(!transfers[_transferId].sent, "This transfer has already been sent.");
        
        walletBalance -= transfers[_transferId].amount;
        transfers[_transferId].sent = true;
        transfers[_transferId].sentBy = msg.sender;
        transfers[_transferId].sentAt = block.timestamp;
        
        payable(transfers[_transferId].beneficiary).transfer(transfers[_transferId].amount);
        
        emit TransferSent(msg.sender, transfers[_transferId].beneficiary, transfers[_transferId].amount);
    }
}
1 Like

hey @joey. so you actually can use this approach

contract Wallet {

    //address array to store owners
    // address[] owners;
    //conensus limit for approvals
    uint public minSignatures;
    
    address[] owners;
    constructor(uint _minSignatures, address owner1, address owner2, address owner3){
        minSignatures = _minSignatures;
        owners.push(owner1);
        owners.push(owner2);
        owners.push(owner3);
    }
}

this deploys. Maybe there is another issue in your code. still i think the passing in of an array is better. It sgreat to see your experimenting thats the best way to learn and curiosity is always key when coding so good work. But there is something you dont understand now that you will when you take solidity 201. You are playing around with these things because you want it to be more user firendly for ppl to imput their addresses. What you must note is that remix is an IDE. The deployment tab has nothing to do with the application other than it deployed your code. Remix is a tool for learning solidity and for doing very quick testing. However when you grow you will be using truffle and say VS code as your editor. That deployment tab you see in remix gets reoplaced by whats called a migrations file which will deploy your code. It look something like the code below

const Migrations = artifacts.require("Migrations");

module.exports = function(deployer) {
  deployer.deploy(Migrations);
};

And the areguments that you currently are passing into the constructor in the remix IDE get passed into this code here. So for you it would be something like

const multisig = artifacts.require("Multisig");

module.exports = function(deployer) {
  const userAddress1 = accounts[1];

  deployer.deploy(MultiSig, userAddress1)
};

something akin to that. So you can see that you should focus on using the most optimal approach in terms of good good which cost the least gas, So rhere is no need to focus on getting the solidity remix IDE to be mor euser firendly when passing in constructor arguments as it has no effect on code optimisation or anything else really. I have done a bad job explaining this but you will see when you start using truffle in sol 201.

But none the less the snippet of code i have at the top of this post works if you want to continue using this style for deployment

here is my solution to the Multisig assignment.
I tried to solve it without watching the additional assistance videos.

Here are the main ideas:
The contract owner defines n required approvers out m multisig owners, when the contract is deployed
The contract owners is also able to add the required multisig owners calling the function addMultisigOwner.

The configured multisig owners are able to create a transfer requests calling the function createTransferRequest
Multisig owners can also approve a transfer request calling the function approveTransferRequest.

Current limitations:
at the moment there is no code to enforce that the contract owner would configures the correct number of multisig owners and to ensure that multisig owners won’t create transaction requests before the required number of multisig owners have been configured.

FIle Ownable.sol

pragma solidity 0.7.5;

contract Ownable {

    address payable owner;
        
    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    constructor() {
        owner = msg.sender;
    }
}

File Destroyable.sol

pragma solidity 0.7.5;

import "./Ownable.sol";

contract Destroyable is Ownable {

    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

File Multisig.sol

pragma solidity 0.7.5;
pragma abicoder v2;

import "./Destroyable.sol";

contract Multisig is Destroyable {
    

    event transferCreated(uint indexed transferId, uint amount, address indexed destination);
    event transferApproved(uint indexed transferId, address indexed approver);
    event transferSent(uint indexed transferId, uint amount, address indexed destination);
    event depositedAmount(address indexed from, uint amount);
    
    struct Transfer {
        uint id;
        uint amount;
        address payable destination;
        bool isPaid;
    }
    
    
    uint owners;    // the number of owners of the multisig
    uint approvals; // the number of approvals required for a transfer
    
    uint transfersCount = 1; // used to produce transfer identifiers
    
    mapping(uint => Transfer) pendingTransfers;    // maps a transferId to a pending transfer request
    mapping(address => bool) ownersAddresses;  // the address of the owners of the multisig
    mapping(uint => mapping(address => bool)) transferApprovedAddresses; // maps a transferId to the multisig owners addresses that approved that transfer
    mapping(uint => uint) transferApprovedCount;  // maps a transferId to the number of multisig owners that approved that transfer
    

    modifier onlyMultisigOwner() {
        require(ownersAddresses[msg.sender] == true);
        _;
    }

        
    constructor( uint _approvals, uint _owners) {
        require(_approvals > 0, "Need at least 1 approval");
        require(_owners > 0, "Need at least 1 owner");
        require(_approvals <= _owners, "Invalid approvals");
        
        owners = _owners;
        approvals = _approvals;
    }
    
    
    function getPendingTransfer(uint transferId) public view returns(Transfer memory) {
        
        return pendingTransfers[transferId];
    }
    
    function getApproverStatus(uint transferId, address approver) public view returns(bool) {
        
        return transferApprovedAddresses[transferId][approver];
    }
    
    
    function deposit() public payable returns (uint) {
    
        emit depositedAmount(msg.sender, msg.value);
    }
    
    function addMultisigOwner(address owner) public onlyOwner {
        require(ownersAddresses[owner] == false, "Owner address already added");
        
        ownersAddresses[owner] = true;
    }
    
    
    function createTransferRequest(address payable _destination, uint _amount) public onlyMultisigOwner returns (uint) {
        
        Transfer memory transfer = Transfer(transfersCount, _amount, _destination, false);
        pendingTransfers[transfersCount] = transfer;
        transfersCount++;
        
        emit transferCreated(transfer.id, transfer.amount, transfer.destination);
        
        return transfer.id;
    }
    
    function approveTransferRequest(uint transferId) public onlyMultisigOwner {
        
        require(pendingTransfers[transferId].id != 0, "Invalid transferId");
        
        _approveRequest(transferId, msg.sender);
        
        // if the transfer reached the required count trigger the payment
        Transfer storage transfer = pendingTransfers[transferId];
        if (transfer.isPaid == false && transferApprovedCount[transferId] == approvals) {
            transfer.isPaid = true;
            _transfer(transfer.id, transfer.amount, transfer.destination);
        }
    }
    
    
    // approves the transfer request by one of multisig owner 
    function _approveRequest(uint transferId, address approver) private {
        
        require(transferApprovedAddresses[transferId][approver] == false, "Transfer already approved");
        
        transferApprovedAddresses[transferId][approver] = true; // keep track of this approver 
        transferApprovedCount[transferId] += 1;                 // increment required approval count
        
        emit transferApproved(transferId, approver);
    }
    
    
    function _transfer(uint transferId, uint amount, address payable destination) private {
        destination.transfer(amount);
        
        emit transferSent(transferId, amount, destination);
    }
   
}
2 Likes

So I had a crack at this after watching the first video. I am not a very confident coder in this language, so I had to go through several github codes and solidity stack overflow demonstrations to assist. Hope that is alright.
That said, here is my code below:

pragma solidity 0.7.5;

// 2 out of 3 sig Wallet

contract Wallet {
    //create the events for each part of the Wallet functionality
    event Deposit(address indexed sender, uint amount, uint balance);
    //propose transaction that must be approved by other owners
    event SubmitTransaction(
        address indexed owner,
        uint indexed transactionId,
        address indexed to,
        uint value,
        bytes data
        );
        
    event ConfirmTransaction(address indexed owner, uint indexed transactionId);
    
    event RevokeConfirmation(address indexed owner, uint indexed transactionId);
    
    event ExecuteTransaction(address indexed owner, uint indexed transactionId);
    
    //create the transaction structure
    struct Transaction {
        address to;
        uint value;
        bytes data; //if calling another/external contract
        bool executed;
        uint numConfs;
    }
    
    //store an array of 3 owner addresses
    address[3] public owners;
    //store number of confirmations required
    uint public numConfirmsReq;
    //is address owner or not
    mapping(address => bool) public isOwner;
    //mapping from transactionId => owner => bool
    mapping(uint => mapping(address => bool)) public isConfirmed;
    //store an array of transactions
    Transaction[] transactions;
    
    //create the owner constructor
    constructor(address[] memory _owners, uint _numConfirmsReq) {
        require(_owners.length > 0, "Owners required");
        require(_numConfirmsReq > 0 && _numConfirmsReq <= _owners.length, "invalid number of required confirmations");
        
        numConfirmsReq = _numConfirmsReq;
    }
    
    //create the modifiers
    modifier onlyOwner() {
        require(isOwner[msg.sender], "Not the owner");
        _;
    }
    
    modifier txExists(uint _transactionId) {
        require(_transactionId < transactions.length, "Transaction doesn't exist");
        _;
    }
    
    modifier notExecuted(uint _transactionId) {
        require(!transactions[_transactionId].executed, "Transaction already executed");
        _;
    }
    
    modifier notConfirmed(uint _transactionId) {
        require(!isConfirmed[_transactionId][msg.sender], "Transaction already confirmed");
        _;
    }
    
    //create the functions
    receive() payable external {
        emit Deposit(msg.sender, msg.value, address(this).balance);
    }
    
    function deposit() payable external {
        emit Deposit(msg.sender, msg.value, address(this).balance);
    }
        
    function submitTransaction(address _to, uint _value, bytes memory _data) public onlyOwner {
        uint transactionId = transactions.length;
        transactions.push(Transaction({
            to: _to,
            value: _value,
            data: _data,
            executed: false,
            numConfs: 0
        }));
        emit SubmitTransaction(msg.sender, transactionId, _to, _value, _data);
    }
    
    function confirmTransaction(uint _transactionId) public
    onlyOwner
    txExists(_transactionId) 
    notExecuted(_transactionId)
    notConfirmed(_transactionId) {
        Transaction storage transaction = transactions[_transactionId];
        isConfirmed[_transactionId][msg.sender] = true; //msg.sender has approved this transaction
        transaction.numConfs += 1;
        require(transaction.numConfs == 2, "Need at least 2 confirmations for your transaction");
        emit ConfirmTransaction(msg.sender, _transactionId);
    }
    
    function executeTransaction(uint _transactionId) public
    onlyOwner
    txExists(_transactionId)
    notExecuted(_transactionId) {
        Transaction storage transaction = transactions[_transactionId];
        require(transaction.numConfs >= numConfirmsReq, "Can't execute the transaction");
        transaction.executed = true;
        (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
        require(success, "Transaction failed");
        emit ExecuteTransaction(msg.sender, _transactionId);
    }
    
    function revokeConfirmation(uint _transactionId) public
    onlyOwner
    txExists(_transactionId)
    notExecuted(_transactionId) {
        Transaction storage transaction = transactions[_transactionId];
        require(isConfirmed[_transactionId][msg.sender], "Transaction not confirmed");
        transaction.numConfs -= 1;
        isConfirmed[_transactionId][msg.sender] = false;
        emit RevokeConfirmation(msg.sender, _transactionId);
    }
    
    function getOwners() public view returns(address[3] memory) {
        return owners;
    }
    
    function getTransactionCount() public view returns(uint) {
        return transactions.length;
    }
    
    function getTransaction(uint _transactionId)
    public 
    view
    returns(address to, uint value, bytes memory data, bool executed, uint numConfs) {
        Transaction storage transaction = transactions[_transactionId];
        return (
            transaction.to,
            transaction.value,
            transaction.data,
            transaction.executed,
            transaction.numConfs
            );
    }
        
}

Please let me know what I could do better.
Appreciate it :+1:

1 Like

Here I added these improvements to the original Multisig.sol contract code:

  • Renamed owners of the multisig to signers.
  • Ensure no more signers addresses can be added to the multisig than the max number of signers m defined when the contract is deployed.
  • Ensure all signer addresses are added to the multisig before signers are allowed to create transfer requests
pragma solidity 0.7.5;
pragma abicoder v2;

import "./Destroyable.sol";

contract Multisig is Destroyable {
    
    event transferCreated(uint transferId, uint amount, address destination);
    event transferApproved(uint transferId, address approver);
    event transferSent(uint transferId, uint amount, address destination);
    event depositedAmount(address from, uint amount);
    
    struct Transfer {
        uint id;
        uint amount;
        address payable destination;
        bool isPaid;
    }
    
    
    uint signers;   // the nubmer of signers of the multisig
    uint approvals; // the number of approvals by the signers required for a transfer
    
    uint signersCount; // the number of signers added to this multisig 
    uint transfersCount = 1; // used to produce tranfer identifiers

    
    mapping(uint => Transfer) pendingTransfers;    // maps a transferId to a pending transfer request
    mapping(address => bool) signerAddresses;  // the addresss of the signers of the multisig
    
    mapping(uint => mapping(address => bool)) transferApprovedAddresses; // maps a transferId to the signers' addresses that approved that transfer
    mapping(uint => uint) transferApprovedCount;  // maps a transferId to the number of multisig signers that approved that transfer
    

    modifier onlyMultisigSigner() {
        require(signerAddresses[msg.sender] == true);
        _;
    }

        
    constructor( uint _approvals, uint _signers) {
        require(_approvals > 0, "Need at least 1 approval");
        require(_signers > 0, "Need at least 1 signer");
        require(_approvals <= _signers, "Can't have more approvers than signers");
        
        signers = _signers;
        approvals = _approvals;
    }
    
    
    function getPendingTransfer(uint ransferId) public view returns(Transfer memory) {
        
        return pendingTransfers[ransferId];
    }
    
    function getApproverStatus(uint transferId, address approver) public view returns(bool) {
        
        return transferApprovedAddresses[transferId][approver];
    }
    
    function deposit() public payable returns (uint) {
    
        emit depositedAmount(msg.sender, msg.value);
    }
    
    function addMultisigSigner(address signer) public onlyOwner {
        require(signersCount < signers, "All signers already added");
        require(signerAddresses[signer] == false, "Owner address already added");
        
        signerAddresses[signer] = true;
        signersCount++;
    }
    
    function createTransferRequest(address payable _destination, uint _amount) public onlyMultisigSigner returns (uint) {
        
        require(signersCount == signers, "Can't create a transfer request until all signers are added");
        
        Transfer memory transfer = Transfer(transfersCount, _amount, _destination, false);
        pendingTransfers[transfersCount] = transfer;
        transfersCount++;
        
        emit transferCreated(transfer.id, transfer.amount, transfer.destination);
        
        return transfer.id;
    }
    
    function approveTransferRequest(uint transferId) public onlyMultisigSigner {
        
        require(pendingTransfers[transferId].id != 0, "Invalid transferId");
        
        _approveRequest(transferId, msg.sender);
        
        // if the transfer reached the required count trigger the payment
        Transfer storage transfer = pendingTransfers[transferId];
        if (transfer.isPaid == false && transferApprovedCount[transferId] == approvals) {
            transfer.isPaid = true;
            _transfer(transfer.id, transfer.amount, transfer.destination);
        }
    }
    
    function _approveRequest(uint transferId, address approver) private {
        
        require(transferApprovedAddresses[transferId][approver] == false, "Tranfer already approved");
        
        transferApprovedAddresses[transferId][approver] = true; // keep track of this approver 
        transferApprovedCount[transferId] += 1;                 // inrement required approval count
        
        emit transferApproved(transferId, approver);
    }
    
    function _transfer(uint transferId, uint amount, address payable destination) private {
        destination.transfer(amount);
        
        emit transferSent(transferId, amount, destination);
    }
}
1 Like

Hi!

Here is my code for Multisig wallet which has 3 owners. I also added option if all 3 owners to approve self-destruction. If it is approved contract will self-destruct and all funds will be send to person who deployed smart contract.

Code:

pragma solidity 0.7.5;
pragma abicoder v2;


contract MultiSigWallet {
    
    //Variables, mappings, ...
    address[] public owners; 
    uint limit; 
    uint public selfDestructAprovalsV = 0;
    address payable public president;
    mapping(address => uint) balance;
    mapping(address => mapping(uint => bool)) approvals;
    mapping(address => bool) selfDestructAprovalsM;
    event depositDone(uint amount, address depositedTo);
    event proposalAproved(uint proposalId, bool TorF);
    event transferDone(uint proposalId, address to, uint amount);
    
    
    constructor(address _ownerOne, address _ownerTwo, address _ownerThree, uint _limit){
        owners.push(_ownerOne);
        owners.push(_ownerTwo);
        owners.push(_ownerThree);
        limit += _limit;
        selfDestructAprovalsV = 0;
        president = msg.sender;
    }

    modifier onlyOwners(){
        require(msg.sender == owners[0] ||
                msg.sender == owners[1] ||
                msg.sender == owners[2]);
        _;
    }
    
    struct Transfer {
        address payable to;
        uint amount;
        uint numOfAprovals;
        bool sent;
    }
    
    Transfer[] public transferProposals;
    
    

    //FUNCTIONS:
    function deposit() public payable onlyOwners {
        balance[msg.sender] += msg.value;
        balance[address(this)] += msg.value;
        emit depositDone(msg.value, msg.sender);
    }
    
    function AproveSelfDestruct() public onlyOwners {
        require(selfDestructAprovalsM[msg.sender] == false);
        selfDestructAprovalsV += 1;
        selfDestructAprovalsM[msg.sender] = true;
        
        if (selfDestructAprovalsV == 3){
            selfdestruct(president);
        }
    }
    
    function createTransferProposal(address payable _to, uint _amount) public onlyOwners {
        transferProposals.push(Transfer(_to, _amount, 0, false));
    }
    
    function aproveProposal(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] == false);
        require(transferProposals[_id].sent == false);
        
        emit proposalAproved(_id, approvals[msg.sender][_id]);
        approvals[msg.sender][_id] = true;
        transferProposals[_id].numOfAprovals ++;
        
        if(transferProposals[_id].numOfAprovals == limit){
            require(balance[address(this)] >= transferProposals[_id].amount);
            transferProposals[_id].to.transfer(transferProposals[_id].amount);
            transferProposals[_id].sent = true;
            emit transferDone(_id, transferProposals[_id].to, transferProposals[_id].amount);
        }
    }
    
    
    
    //GET FUNCTIONS:
    function getAddressBalance() public view returns(uint){
        return balance[msg.sender];
    }

    function getContractBalance() public view returns(uint){
        return balance[address(this)];
    }
}
1 Like

Hi Evan,

Thank you so much for your feedback!

I’ve successfully added a function for deleting owners following the logic you outlined. On the second point in your feedback, I put an array in the Transfer struct for the purpose of keeping a record of all the addresses that have given approval for a transfer. I think a more detailed audit trail should be available in an application that deals with money. I was wondering if we can trace all approvals history without a separate depository of storing them.

Thanks again; I’ve learned a lot from your feedback.

Eric

2 Likes
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

import "./Destroyable.sol";

contract Multisig is Destroyable {

    uint public _balance = 0;
    uint _minApprovals = 0;
    
    mapping(address => bool) private signers;

    struct Transaction {
        bool exists;
        bool processed;
        uint amount;
        uint numApprovals;
        mapping(address => bool) signers;
        address destAddr;
    }
    
    uint _txIdCount = 0;
    mapping(uint => Transaction) private transactions;

    event onDeposit(address indexed depositAddress, uint amount);

    constructor(uint minApprovals) {
        _minApprovals = minApprovals;
        signers[msg.sender] = true;
    }
    
    function deposit() public payable {
        require(msg.value > 100000);
        _balance += msg.value;
        emit onDeposit(msg.sender, msg.value);
    }
    
    function transfer(uint amount, address destAddr) public returns(uint) {
        require(signers[msg.sender]);
        require(_balance > amount);
        
        Transaction storage transaction = transactions[_txIdCount];
        transaction.exists = true;
        transaction.processed = false;
        transaction.amount = amount;
        transaction.numApprovals = 1;
        transaction.signers[msg.sender] = true;
        transaction.destAddr = destAddr;
        
        return _txIdCount++;
    }

    function approveTransaction(uint txid) public {
        require(signers[msg.sender]);

        Transaction storage transaction = transactions[txid];        
        require(transaction.exists);
        require(!transaction.processed);
        require(!transaction.signers[msg.sender]);
        
        transaction.numApprovals++;
        transaction.signers[msg.sender] = true;
        
        if (transaction.numApprovals >= _minApprovals)
        {
            _balance -= transaction.amount;
            transaction.processed = true;
            payable(transaction.destAddr).transfer(transaction.amount);
        }
    }

    function addSigner(address signer) public onlyOwner {
        require(signer != owner);
        signers[signer] = true;
    }
    
    function setMinApprovals(uint minApprovals) public onlyOwner {
        require(minApprovals > 0);
        _minApprovals = minApprovals;
    }
}
1 Like

I feel like I’m stuck right now, I have already made the majority of the code so far, but the problem is I’m not really sure on how to proceed on checking consensus so then I can evaluate the vote count.

Any hints or recommendations?

PSA: I am trying to make the wallet more flexible by not limiting the amount of users.

My code so far:

multisig.sol:

pragma solidity 0.7.5;
import "./isOwner.sol";
import "./validOwner.sol";

contract multisig is validOwner, isOwner{
    address private _owner;
    
    struct user{
        uint id;
        uint balance;
    }
    
    struct transaction {
        bool sent;
        uint amount;
        uint approvals;
        uint id;
    }
    
    mapping(address => uint8) private _owners;
    mapping(address => uint) balance;
    mapping(address => mapping(uint16 => bool)) approvals;
    
    event depositDone(uint amount, address from);
    event withdrawDone(uint amount, address to);
    event transferDone(uint amount, address to, address from);
    
    function MultiSigWallet() public{
        _owner = msg.sender;
    }
    
    function addOwner(address owner) isOwner public {
        _owners[owner] = 1;
    }
    
    function collectApprovals(uint approvals) validOwner public {
        
    }
    
    
    function deposit() public payable returns (uint){
       balance[msg.sender] += msg.value;
       emit depositDone(msg.value, msg.sender);
       return balance[msg.sender];
   }
   
    function withdraw(uint amount) validOwner public {
        require(address(this).balance >= amount);
        msg.sender.transfer(amount);
        emit withdrawDone(amount, msg.sender);
    }
    
    function transfer(address to, uint amount) validOwner public {
        require(address(this).balance >= amount);
        
        msg.sender.transfer(amount);
        emit transferDone(amount, to, msg.sender);
    }
   
}

validOwner.sol:

contract validOwner{
    address private _owner;
    
    mapping(address => uint8) private _owners;
    
     modifier validOwner() {
        require(msg.sender == _owner || _owners[msg.sender] == 1);
        _;
    }
}

isOwner.sol:

import "./validOwner.sol";

contract isOwner is validOwner{
    address private _owner;
    
    modifier isOwner() {
        require(msg.sender == _owner);
        _;
    } 
}
1 Like