Project - Multisig Wallet

I added a few additional functions for when i was troubleshooting. However one thing I couldnt seem to figure out (and later realised it wasnt part of the scope of this assignment) was that I was trying to make it so that the user of the createTransfer function couldnt also approve the transfer. I added the code:

approvals[msg.sender][transferRequests.length] = true;

to the createTransfer function however it would work for all instanced except for the very first instance when transferRequests.length = 0. No matter how many times i tried it wouldnt change the value from false to true. I couldnt figure it out so I just left it as is. for all subsequent instances it works fine so im a bit baffled. If anyone can explain to me why this is happening I would greatly appreciated it. Thanks!

anyways here is my code with all the troubleshooting functions that i left in as well:

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    
    address[] owners;
    uint limit;

    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    modifier onlyOwners() {
        bool owner = false;
        for (uint i = 0; i<owners.length; i++){
            if(owners[i] == msg.sender) owner = true;
        }
        require(owner == true,"Sorry, you cannot make this transaction as you are not the owner of the contract.");
        _;
    }
    
    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approvals;
    mapping(address => uint) balance;
        
    event transferCreated(address receivingAddress, uint transferAmount);
    event transferApproval(address approvalAddress, uint transferId);
    event transferSent(address approvalAddress, uint transferId, address receivingAddress, uint transferAmount);
    
    function deposit() public payable returns(uint) {
        return address(this).balance;
    }   

    function createTransfer(uint _amount, address payable _receiver) public onlyOwners{
        require(address(this) != _receiver, "Don't transfer money to yourself");
        transferRequests.push( Transfer(_amount, _receiver, 0, false, transferRequests.length));
        approvals[msg.sender][transferRequests.length] = true;
        emit transferCreated(_receiver, _amount);
    }
    
    function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] != true,"You are either the creator or have already approved this transaction.");
        require(transferRequests[_id].id == _id, "This transaction id does not exist.");
        require(transferRequests[_id].hasBeenSent != true, "This transaction has already been sent.");
        require(address(this).balance >= transferRequests[_id].amount, "Balance not sufficient");

        if(transferRequests[_id].approvals < limit-1) {
            transferRequests[_id].approvals ++;
            approvals[msg.sender][_id] = true;
            emit transferApproval(msg.sender, _id);
        }
        else {
            transferRequests[_id].approvals ++;
            approvals[msg.sender][_id] = true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
            transferRequests[_id].hasBeenSent = true;
            balance[transferRequests[_id].receiver] += transferRequests[_id].amount;
            emit transferSent(msg.sender, _id, transferRequests[_id].receiver, transferRequests[_id].amount);
        }
    }
    
    function checkOwnerApprovals(uint _id) public onlyOwners view returns(bool) {
        return approvals[msg.sender][_id];
    }
    
    function getContractInfo() public view returns(uint, address[] memory, uint, Transfer[] memory) {
        return (address(this).balance, owners, limit, transferRequests);
    }
    
    function getTransferInfo(uint _id) public view returns(Transfer memory) {
        return transferRequests[_id];
    }
    
    function getBalance() public view returns (uint){
        return balance[msg.sender];
    }
    
}
1 Like

try go back and rewatch the entire Ethereum101 course again and ask yourself every step along the way whether you have honestly grasped the concepts. If not you should rewatch those videos and search google until you get it, then move on. I found doing this really helped me. Also doing the javascript 101 course beforehand helped heaps. also this course on c++ was a mind bender but it also helped: https://www.youtube.com/watch?v=mUQZ1qmKlLY

keep going man u will get it eventually.

1 Like

Thanks for a little bit of motivation. I already took a JS course, I kinda know how the programming works but just can’t match all the dots like, here in Solidity. I will try to take that course once more with your advices. Happy Easter!

1 Like

Hey @KoenV, hope you are ok.

I have tested your contract, it works good but there are some minor errors that you can fix to improve your contract :face_with_monocle:

Your deposit function should not need to specify an extra amount, with the payable keyword should be more than enough to receive funds, then you can just use the mapping to add the amount to the proper owner.

    function deposit(uint _amount) public payable{
        balance[owner[0]] += _amount;
    }

Would be better if you use it this way:

    function deposit() public payable{
        balance[msg.sender] += msg.value;
    }

Also, there is no way to withdraw the funds from the contract, I manage to create a transfer, approve it with owner 1 and 2, transactions was approved and sent properly, but only inside the contract.

Overall it does exactly what we ask in the assignment, nice job.

Normal signature:

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

Carlos Z.

You have 2 minor issues in your approve function.

    function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] == false); //basically saying that in order for this to GET approved, the msg.sender hasn't approved it yet
        require(transferRequests[_id].hasBeenSent == false);
        
        approvals[msg.sender][_id] = true;
        transferRequests[_id].approvals += 1;
        
        if (transferRequests[_id].approvals >= limit){
            transferRequests[_id].hasBeenSent == true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount); //.transfer is a built in function that sends spec amount
            _transfer(transferRequests[_id].fromPerson, transferRequests[_id].receiver, transferRequests[_id].amount);
        
        }
        
    }

your if condition (transferRequests[_id].approvals >= limit), approvals should be greater or equal to limit, it could be better if you use (transferRequests[_id].approvals == limit) since your tx request should reach to the limit once enough owners approve it.

Then here you are not assigning a value, you are comparing it, you should just use = to assign the boolean to true.

transferRequests[_id].hasBeenSent == true;

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

Carlos Z.

Hey @Bartek, hope you are ok.

Solidity can be complex at the start (learning programming) but from my opinion, programming requires a mindset that you can only develop by practice, the more you code, the more you will understand few basics tricks and logic from coding.

I think you can just rewatch the course to learn deeply some of the concepts that maybe you did not understand quite well, the course have the entire basics about solidity, is just about practice to get used to some of the guidelines to follow.

Carlos Z

1 Like

Hey @phyx1u5, hope you are ok.

You contract works very nice, the only minor issue i catch (based on your comments) is on your createTransfer function, you are already assigning approvals[msg.sender][transferRequests.length] = true; which should be change to true in your approve function, so i just change it to false and then it works great.

I was able to create 2 transfer requests, both was approved by 2 owners properly and sent to the receiver without any issue, so I guess the issue you had is based on that minor assign of value.

image

Carlos Z

1 Like

/*
In this project, you should build a Multisig Wallet Smart Contract. A multisig wallet is a wallet where multiple “signatures” or approvals are needed for an outgoing transfer to take place. As an example, I could create a multisig wallet with me and my 2 friends. I configure the wallet such that it requires at least 2 of us to sign any transfer before it is valid. Anyone can deposit funds into this wallet. But as soon as we want to spend funds, it requires 2/3 approvals.

Here are the requirements of the smart contract wallet you will be building

- Anyone should be able to deposit ether into the smart contract
- The contract creator should be able to input (1): the addresses of the owners and (2):  the numbers of approvals required for a transfer, in the constructor. For example, input 3 addresses and set the approval limit to 2.
- Anyone of the owners should be able to create a transfer request. The creator of the transfer request will specify what amount and to what address the transfer will be made.
- Owners should be able to approve transfer requests.
- When a transfer request has the required approvals, the transfer should be sent. 

*/

pragma solidity 0.7.5;
pragma abicoder v2;

contract MUltiSigWallet {

address[3] public owners;
uint limit;
mapping(address => mapping(uint => bool)) public approvals;

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

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

constructor() {
    owners[0] = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    owners[1] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
    owners[2] = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;
    limit = 2;
}

Transfer[] transferRequests;

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

function deposit() public payable returns (uint)  {
}

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 addTransaction(address payable _to, uint _amount) public onlyOwners {
    
    transferRequests.push(Transfer( _to, _amount, 0, false, transferRequests.length));
}

function getTransaction(uint _index) public onlyOwners view returns(Transfer[] memory) {

    return (transferRequests);
}

}

1 Like
pragma solidity 0.7.5;
pragma abicoder v2;

contract MultisigWallet {

    mapping (address => uint) balance;
    address[] public ownerAddresses; 
    
    struct Transfer {
        uint transferID; 
        address receiver;
        uint amount;
        uint approvals;
        bool performed;
    }
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approvals;
    //mapping[msg.sender][txID] = true  to approve
     uint data;
     uint approvalsRequired;
    
    //The contract creator should be able to input 
    // (1): the addresses of the owners and (2): the numbers of approvals required for a transfer
      constructor(address[] memory _ownerAddresses, uint _approvalsRequired){
        require(_approvalsRequired <= _ownerAddresses.length, "Owner addresses are less than approvals required");
        require(_approvalsRequired >= 1, "An approval is required at least");
        for (uint i = 0; i < _ownerAddresses.length; i++) {
            ownerAddresses.push(_ownerAddresses[i]);
        }
        approvalsRequired = _approvalsRequired;
    }
    
     modifier onlyOwner {
         bool isOwnerAddress = false;
          for (uint i = 0; i < ownerAddresses.length; i++) {
            if(ownerAddresses[i] == msg.sender ) {
                isOwnerAddress = true;
            }
        }
        require(isOwnerAddress, "Only owners can perform this operation");
        _;
    }
    
    // Anyone should be able to deposit ether into the smart contract
    function deposit() public payable returns (uint){
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }
    
    function getBalance() public view returns (uint){
        return balance[msg.sender];
    }
    
    function getTransferRequest(uint transferID) public onlyOwner returns (Transfer memory){
        require(transferID < transferRequests.length, "transfer ID not exists");
        return transferRequests[transferID];
    }
    
    // Anyone of the owners should be able to create a transfer request
    function createTransferRequest(address recipient, uint amount) public payable  onlyOwner returns (uint){
        uint transferID = transferRequests.length;
        transferRequests.push(Transfer(transferID, recipient, amount, 0, false));
        approveWithdraw(transferID); //approve during creation of transfer request
    }
    
    function approveWithdraw(uint transferID) public payable  onlyOwner returns (uint){
         require(transferID < transferRequests.length , "transfer id not exists");
         require(approvals[msg.sender][transferID] != true, "you have already approved");
         require(!transferRequests[transferID].performed, "Failed: Transfer already performed");
           approvals[msg.sender][transferID] = true;
           transferRequests[transferID].approvals += 1;
            if(approvalsRequired <= transferRequests[transferID].approvals) {
                doTransfer(transferID);
        }
    }
    
      function doTransfer(uint transferID) private returns (Transfer memory){
        address payable receiverAddress =  payable(transferRequests[transferID].receiver);
        receiverAddress.transfer(transferRequests[transferID].amount);
        transferRequests[transferID].performed = true;
        return transferRequests[transferID];
    }
} 
1 Like

Took me 3hrs!! (without watching hint videos!)

Feature:

  • Supports any number of owners
  • The contract creator can input (1) and (2) at the sidebar (not in the code). The format is shown below.
  • Only the owners can deposit, request transfer, and approve transfer.
  • Anyone can see the balance of the wallet.
  • Approve transfer request always reset to default (false) for every owners after a transfer.
  • Owners cannot approve transfer request when there is no transfer request (cannot approve beforehand).
  • One owner can approve transfer request only one time. No double approving!

Drawback of my code (realized after I watched solution video):

  • Only one transaction request at a time. (No id)

Deploy input example (quotation mark is required because the input is written in JSON format) :

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

Transfer request input example:

0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db, 2000000000000000000

Approve transfer request: click the orange approveTransfer button

My code (an advice would be really appreciated!):

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

contract Wallet{
    uint balance;
    uint minApprovals;
    bool requested = false;
    address recipient;
    uint amount;
    
    mapping(address => uint) index;
    struct Owner{
        address user;
        bool accept;
    }
    Owner[] owners;
    
    constructor(address[] memory _addresses, uint _minApprovals){
        for(uint i=0;i<_addresses.length;i++){
            Owner memory newOwner = Owner(_addresses[i], false);
            owners.push(newOwner);
            index[_addresses[i]] = i;
        }
        minApprovals = _minApprovals;
    }
    
    modifier onlyOwners{
        bool isOwner = false;
        for(uint i=0;i<owners.length;i++){
            if(msg.sender==owners[i].user){
                isOwner = true;
                break;
            }
        }
        require(isOwner);
        _;
    }
    
    
    function deposit() public payable onlyOwners {
        balance += msg.value;
    }
    
    function transferRequest(address _recipient, uint _amount) public onlyOwners{
        resetApproval();
        recipient = _recipient;
        amount = _amount;
        require(balance >= amount, "Balance not sufficient");
        requested = true;
        owners[index[msg.sender]].accept = true;
    }
    
    function approveTransfer() public onlyOwners{
        require(requested);
        owners[index[msg.sender]].accept = true;
        uint count=0;
        for(uint i=0;i<owners.length;i++){
            if(owners[i].accept == true){ 
                count++;
            }
        }
        if(count >= minApprovals){
            transfer();
        }
    }
    
    function transfer() private{
        require(balance >= amount, "Balance not sufficient");
        payable(recipient).transfer(amount);
        balance -= amount;
    }
    
    function resetApproval() private{
        for(uint i=0;i<owners.length;i++){
            owners[i].accept = false;
        }
        requested = false;
    }
    
    function getBalance() public view returns(uint){
        return balance;
    }
}

Screen Shot 2021-04-05 at 21.49.14

1 Like
pragma solidity 0.7.5;

/**
 * Contracts shared by multiple owners.
 */
contract Shared {
    
     // This contract owners.
    address[] owners;
    
    /**
     * Defines a function that con be executed only by owners.
     */
    modifier onlyOwner {
        bool isOwner = false;
        for (uint i = 0; i < owners.length; i++){
            if (owners[i] == msg.sender){
                isOwner = true;
                break;
            }
        }
        require (isOwner,"Only contract owners can execute this function.");
        _; // run the function
    }
    
    /**
     * @param additionalOwners Owners of this contract in addition to it's creator.
     */
    constructor(address[] memory additionalOwners) {
        require (additionalOwners.length > 0, "This is a multisig contract, inform at least one additional address");
        owners.push(msg.sender);
        for (uint i = 0; i < additionalOwners.length; i++){
            if (additionalOwners[i] == msg.sender){
                require(false,"The additional owners can not include the creator.");
            }
            owners.push (additionalOwners[i]);
        }
    }
}

pragma solidity 0.7.5;
pragma abicoder v2;
import "./Shared.sol";

/**
 * Contracts that require multiple aprovals for a withdraw.
 */
contract MultisigWallet is Shared {

    /**
     * A withdraw representation.
     */
    struct Withdraw {
        // sequencial id of the withdraw.
        uint idx;
        // address to whitch the withdraw was requested.
        address payable toAddress;
        // amount to be withdrawed.
        uint amount;
        // num aprovals already done for the withdraw.
        uint numAprovals;
    }
    
    // pending withdraws.
    Withdraw[] private withdraws;    
    
    // Number of withdraws pending of aproval.
    uint numWithdraws;
    
    // withdraws pending indexed by owners.
    mapping (address => mapping(uint => bool)) private withdrawsToAprove;
    
    // withdraws array indexed by withdraw idx.
    mapping (uint => uint) private withdrawsById;
   
    // sequencial id for the next withdraw.
    uint private withdrawId = 0;
    
    // Minimal number of aprovals for a withdraw in this contract.
    uint private aprovalMin;

    /**
     * @param aprovalMin_ initializes aprovalMin.
     */
    constructor (address[] memory additionalOwners, uint aprovalMin_) Shared(additionalOwners) {
        require(aprovalMin_ > 0, "The number of aprovals must be greater or equal to 1.");
        require(aprovalMin_ <= additionalOwners.length + 1, 
            "The number of aprovals can not exceed the number of owners.");
        aprovalMin = aprovalMin_;
    }
    
    /**
     * Allows the deposit in this contract.
     */
    function deposit () public payable returns (uint){
        return address(this).balance;
    }
    
    /**
     * @return the current balance of this contract.
     */
    function getBalance() public view returns (uint){
        return address(this).balance;
    }
    
    /**
     * Inits a withdraw.
     * @param toAddress withdraw destination.
     * @param amount value to be withdrawed.
     */
    function initWithdraw (address payable toAddress, uint amount) public onlyOwner {
        require (amount > 0, "Widthdraw amount must be greater than zero.");
        Withdraw memory withdraw = Withdraw(withdrawId++, toAddress, amount, 0);
        addWithdraw (withdraw);
        aprove(withdraw.idx);
    }
    
    /**
     * @return the withdraws to be aproved by the sender.
     */
    function getPendingAprovals() public view onlyOwner returns (Withdraw[] memory){
        uint size = 0;
        for (uint i = 0; i < numWithdraws; i++){
            if (!isAprovedBy(msg.sender, withdraws[i].idx)){
                size++;
            }
        }
        Withdraw[] memory withdrawsToReturn = new Withdraw[](size);
        for (uint i = 0; i < numWithdraws; i++){
            if (!isAprovedBy(msg.sender, withdraws[i].idx)){
                withdrawsToReturn[--size] = withdraws[i];
            }
        }
        return withdrawsToReturn;
    }
    
    /**
     * Aproves the withdraw with the informed identifier.
     * @param idx identifier of the withdraw to be aproved.
     */
    function aprove (uint idx) public onlyOwner {
        require (!isAprovedBy(msg.sender,idx), "Withdraw is not pending");
        // Sender is not pendent anymore.
        delete withdrawsToAprove[msg.sender][idx];
        Withdraw storage withdrawToAprove = getWithdraw(idx);
        withdrawToAprove.numAprovals++;
        if (isAproved (withdrawToAprove)) {
            doWithdraw (withdrawToAprove.toAddress, withdrawToAprove.amount);
            removeWithdraw (idx);
        }
    }
    
    /**
     * Defines if the informed withdraw is aproved.
     * @return true if the withdraw is aproved.
     */
    function isAproved (Withdraw storage  withdraw_) private view returns (bool) {
        return withdraw_.numAprovals >= aprovalMin;
    }

    /**
     * Executes the withdraw.
     * @param toAddress address to whitch the withdraw will be made.
     * @param amount value to be withdrawed.
     */
    function doWithdraw (address payable toAddress, uint amount) private {
        require (getBalance() >= amount, "Insufficient balance.");
        toAddress.transfer (amount);
    }
    
    /**
     * Removes a withdraw from then control.
     * @param idx identifier of the withdraw to be removed.
     */
    function removeWithdraw (uint idx) private {
        uint pos = withdrawsById[idx]; 
        if (numWithdraws > 1){
            // smart delete
            withdraws[pos] = withdraws[numWithdraws-1];
        }
        numWithdraws--;
        delete withdrawsById[idx];
        for (uint i = 0; i < owners.length; i++) {
            delete withdrawsToAprove[owners[i]][idx];
        }
        assert (numWithdraws <= withdraws.length);
    }
    
    /**
     * Adds a new withdraw to the control.
     * @param withdraw Withdraw to be added.
     */
    function addWithdraw (Withdraw memory withdraw) private {
        withdrawsById[withdraw.idx] = numWithdraws;
        if (numWithdraws == withdraws.length){
            withdraws.push (withdraw);
            numWithdraws++;
        }
        else {
            withdraws[numWithdraws++] = withdraw;
        }
        
        for (uint i = 0; i < owners.length; i++) {
                withdrawsToAprove[owners[i]][withdraw.idx] = true;
        }
        assert (numWithdraws <= withdraws.length);
    }
    
    /**
     * @param owner owner whose aproval is being verified.
     * @param idx id of the withdraw whose aproval is being verified.
     * @return true if the owner has aproved the withdraw with the informed id.
     */
    function isAprovedBy (address owner, uint idx) private view returns (bool){
        return !withdrawsToAprove[owner][idx];
    }
    
    /**
     * @param idx the identifier of the withdraw to be returned.
     * @return the withdraw with the given identifier.
     */
    function getWithdraw (uint idx) private view returns (Withdraw storage){
        return withdraws[withdrawsById[idx]];
    }
}

Hey @REGO350, hope you are ok.

Your contract works very well, I was able to deploy it with 3 owners, _MINAPPROVALS = 2, i approve it with 2/3 different owners and the funds were sent to the recipient correctly (the 3rd owner).

although it can be improved to work with multiple transactions, for now it just work with 1 at the time, but for 3 hours only is a very good assignment.

Now some few suggestions:

    function transferRequest(address _recipient, uint _amount) public onlyOwners{
        resetApproval();
        recipient = _recipient;
        amount = _amount;
        require(balance >= amount, "Balance not sufficient");
        requested = true;
        owners[index[msg.sender]].accept = true;
    }

You should always follow this pattern on solidity: check, effect, interactions. That way, all your requires should on top, then effects like resetApproval or assigning values to the next variables, at last interactions, like the owners mapping value change at the bottom.

    function approveTransfer() public onlyOwners{
        require(requested);
        owners[index[msg.sender]].accept = true;
        uint count=0;
        for(uint i=0;i<owners.length;i++){
            if(owners[i].accept == true){ 
                count++;
            }
        }
        if(count >= minApprovals){
            transfer();
        }
    }

this one works quite well, but im not that sure about the require, what is the purpose for it? Also i was able to approve many times with the same owner, but the transaction was waiting for another owner to approve it in order to sent it, so thats great.

Overall you made a very good job, congratulations!

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

Carlos Z.

Thank you Carlos for the detailed reply!
Later in the evening, I will upgrade it so owners can request multiple transcations.
Also, thank you for pointing out check, effect interactions. I will rewrite in this format from next time.
The require(requested) makes sure that owners are not approving transaction when no transfer request is made. So basically, it prevents owners from voting beforehand. When a transaction is requested, requested becomes true, after the transaction has completed it becomes false. This boolean is required because my code only supports one request at a time.

Rio K.

1 Like

Also I have one more question. In Filip’s code there is this:

transferRequests[_id].hasBeenSent = true;
transferRequests[_id].receiver.transfer(transferRequests[_id].amount);

First he changes value to true (that transfer request was sent) and only after that he sends the transfer. Isn’t it better habbit to do it vice versa? Because there can be some unexpected error in second line so it will not take the place, but the first line is executed (gas is already burned). In this situation we say that transfer was sent and after that we actually send it. It is weird for me. Is there some reason to do it this way? Thanks.

EDIT:
I’ve got an idea. If something goes wrong, table editting can be reverted to its original value. However transfer can’t be reverted. Is this why the transfer is executed as last?

1 Like

Btw, now that I am done with the course, is here anyone who would need help with anything so I can practise more? Or any recommendations where to look?

Or should I continue with advance solidity course / smart contract security straight off and practise afterwards? :wink:

1 Like

New feature:
Possibility of rejecting a withdraw, removing it from the wallet control.


pragma solidity 0.7.5;

/**
 * Contracts shared by multiple owners.
 */
contract Shared {
    
     // This contract owners.
    address[] owners;
    
    /**
     * Defines a function that con be executed only by owners.
     */
    modifier onlyOwner {
        bool isOwner = false;
        for (uint i = 0; i < owners.length; i++){
            if (owners[i] == msg.sender){
                isOwner = true;
                break;
            }
        }
        require (isOwner,"Only contract owners can execute this function.");
        _; // run the function
    }
    
    /**
     * @param additionalOwners Owners of this contract in addition to it's creator.
     */
    constructor(address[] memory additionalOwners) {
        require (additionalOwners.length > 0, "This is a multisig contract, inform at least one additional address");
        owners.push(msg.sender);
        for (uint i = 0; i < additionalOwners.length; i++){
            if (additionalOwners[i] == msg.sender){
                require(false,"The additional owners can not include the creator.");
            }
            owners.push (additionalOwners[i]);
        }
    }
}

pragma solidity 0.7.5;
pragma abicoder v2;
import "./Shared.sol";

/**
 * Contracts that require multiple aprovals for a withdraw.
 */
contract MultisigWallet is Shared {

    /**
     * A withdraw representation.
     */
    struct Withdraw {
        // sequencial id of the withdraw.
        uint idx;
        // address to whitch the withdraw was requested.
        address payable toAddress;
        // amount to be withdrawed.
        uint amount;
        // number of aprovals already done for the withdraw.
        uint numAprovals;
        // number of rejections done for the withdraw.
        uint numRejects;
    }

    // Withdraw proposals responses.
    uint8 PENDING = 0;
    uint8 APROVED = 1;
    uint8 REJECTED = 2;
    
    // pending withdraws.
    Withdraw[] private withdraws;    
    
    // Number of withdraws pending of aproval.
    uint numWithdraws;
    
    // withdraws pending indexed by owners.
    mapping (address => mapping(uint => uint8)) private withdrawsToAprove;
    
    // withdraws array indexed by withdraw idx.
    mapping (uint => uint) private withdrawsById;
   
    // sequencial id for the next withdraw.
    uint private withdrawId = 0;
    
    // Minimal number of aprovals for a withdraw in this contract.
    uint private aprovalMin;

    /**
     * @param aprovalMin_ initializes aprovalMin.
     */
    constructor (address[] memory additionalOwners, uint aprovalMin_) Shared(additionalOwners) {
        require(aprovalMin_ > 0, "The number of aprovals must be greater or equal to 1.");
        require(aprovalMin_ <= additionalOwners.length + 1, 
            "The number of aprovals can not exceed the number of owners.");
        aprovalMin = aprovalMin_;
    }
    
    /**
     * Allows the deposit in this contract.
     */
    function deposit () public payable returns (uint){
        return address(this).balance;
    }
    
    /**
     * @return the current balance of this contract.
     */
    function getBalance() public view returns (uint){
        return address(this).balance;
    }
    
    /**
     * Inits a withdraw.
     * @param toAddress withdraw destination.
     * @param amount value to be withdrawed.
     */
    function initWithdraw (address payable toAddress, uint amount) public onlyOwner {
        require (amount > 0, "Widthdraw amount must be greater than zero.");
        Withdraw memory withdraw = Withdraw(withdrawId++, toAddress, amount, 0, 0);
        addWithdraw (withdraw);
        aprove(withdraw.idx);
    }
    
    /**
     * @return the withdraws to be aproved by the sender.
     */
    function getPendingAprovals() public view onlyOwner returns (Withdraw[] memory){
        uint size = 0;
        for (uint i = 0; i < numWithdraws; i++){
            if (isWaitingAproval(msg.sender, withdraws[i].idx)){
                size++;
            }
        }
        Withdraw[] memory withdrawsToReturn = new Withdraw[](size);
        for (uint i = 0; i < numWithdraws; i++){
            if (isWaitingAproval(msg.sender, withdraws[i].idx)){
                withdrawsToReturn[--size] = withdraws[i];
            }
        }
        return withdrawsToReturn;
    }
    
    /**
     * Aproves the withdraw with the informed identifier.
     * @param idx identifier of the withdraw to be aproved.
     */
    function aprove (uint idx) public onlyOwner {
        require (isWaitingAproval(msg.sender,idx), "Withdraw is not pending");
        withdrawsToAprove[msg.sender][idx] = APROVED;
        Withdraw storage withdrawToAprove = getWithdraw(idx);
        withdrawToAprove.numAprovals++;
        if (isAproved (withdrawToAprove)) {
            doWithdraw (withdrawToAprove.toAddress, withdrawToAprove.amount);
            removeWithdraw (idx);
        }
    }
    
    /**
     * Rejects the withdraw with the informed identifier.
     * @param idx identifier of the withdraw to be rejected.
     */
    function reject (uint idx) public onlyOwner {
        require (isWaitingAproval(msg.sender,idx), "Withdraw is not pending");
        withdrawsToAprove[msg.sender][idx] = REJECTED;
        Withdraw storage withdrawToReject = getWithdraw(idx);
        withdrawToReject.numRejects++;
        if (isRejected (withdrawToReject)) {
            removeWithdraw (idx);
        }
    }
    
    /**
     * Defines if the informed withdraw is aproved.
     * @return true if the withdraw is aproved.
     */
    function isAproved (Withdraw storage  withdraw_) private view returns (bool) {
        return withdraw_.numAprovals >= aprovalMin;
    }
    
    /**
     * Defines if the informed withdraw is rejected.
     * @return true if the withdraw is rejected.
     */
    function isRejected (Withdraw storage  withdraw_) private view returns (bool) {
        return owners.length - withdraw_.numRejects < aprovalMin;
    }

    /**
     * Executes the withdraw.
     * @param toAddress address to whitch the withdraw will be made.
     * @param amount value to be withdrawed.
     */
    function doWithdraw (address payable toAddress, uint amount) private {
        require (getBalance() >= amount, "Insufficient balance.");
        toAddress.transfer (amount);
    }
    
    /**
     * Removes a withdraw from then control.
     * @param idx identifier of the withdraw to be removed.
     */
    function removeWithdraw (uint idx) private {
        uint pos = withdrawsById[idx]; 
        if (numWithdraws > 1){
            // smart delete
            withdraws[pos] = withdraws[numWithdraws-1];
        }
        numWithdraws--;
        delete withdrawsById[idx];
        for (uint i = 0; i < owners.length; i++) {
            delete withdrawsToAprove[owners[i]][idx];
        }
        assert (numWithdraws <= withdraws.length);
    }
    
    /**
     * Adds a new withdraw to the control.
     * @param withdraw Withdraw to be added.
     */
    function addWithdraw (Withdraw memory withdraw) private {
        withdrawsById[withdraw.idx] = numWithdraws;
        if (numWithdraws == withdraws.length){
            withdraws.push (withdraw);
            numWithdraws++;
        }
        else {
            withdraws[numWithdraws++] = withdraw;
        }
        
        for (uint i = 0; i < owners.length; i++) {
            withdrawsToAprove[owners[i]][withdraw.idx] = PENDING;
        }
        assert (numWithdraws <= withdraws.length);
    }
    
    /**
     * @param owner owner whose aproval is being verified.
     * @param idx id of the withdraw whose aproval is being verified.
     * @return true if the withdraw with the informed id is waiting for 
     * aproval from the owner informed.
     */
    function isWaitingAproval (address owner, uint idx) private view returns (bool){
        return withdrawsToAprove[owner][idx] == PENDING;
    }
    
    /**
     * @param idx the identifier of the withdraw to be returned.
     * @return the withdraw with the given identifier.
     */
    function getWithdraw (uint idx) private view returns (Withdraw storage){
        return withdraws[withdrawsById[idx]];
    }
}

How can I do it better?

    /**
     * @return the withdraws to be aproved by the sender.
     */
    function getPendingAprovals() public view onlyOwner returns (Withdraw[] memory){
        uint size = 0;
        for (uint i = 0; i < numWithdraws; i++){
            if (isWaitingAproval(msg.sender, withdraws[i].idx)){
                size++;
            }
        }
        Withdraw[] memory withdrawsToReturn = new Withdraw[](size);
        for (uint i = 0; i < numWithdraws; i++){
            if (isWaitingAproval(msg.sender, withdraws[i].idx)){
                withdrawsToReturn[--size] = withdraws[i];
            }
        }
        return withdrawsToReturn;
    }

Now supports multiple transfer requests:

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.3;
pragma abicoder v2;

contract Wallet{
    uint balance;
    uint minApprovals;
    uint numberOfOwners;
    
    address[] owners;
    
    struct Request {
        uint id;
        address recipient;
        uint amount;
        bool accepted;
        address [] approvers;
    }
    Request[] requests;
    
    event TransferRequestCreated(uint _id, address _initiator, address _recipient, uint _amount);
    event ApprovalRecieved(uint _id, address _approver, uint _approvals);
    event TransferApproved(uint _id);
    
    constructor(address[] memory _addresses, uint _minApprovals){
        for(uint i=0;i<_addresses.length;i++){
            owners.push(_addresses[i]);
        }
        minApprovals = _minApprovals;
    }
    
    modifier onlyOwners{
        bool isOwner = false;
        for(uint i=0;i<owners.length;i++){
            if(msg.sender==owners[i]){
                isOwner = true;
                break;
            }
        }
        require(isOwner, "You are not the owner of this wallet!");
        _;
    }
    
    function deposit() public payable onlyOwners{
        balance += msg.value;
    }
    
    address [] tmp;
    function transferRequest(address _recipient, uint _amount) public onlyOwners{
        require(balance >= _amount, "Balance not sufficient");
        uint n = requests.length;
        emit TransferRequestCreated(n, msg.sender, _recipient, _amount);
        while(tmp.length!=0){
            tmp.pop();
        }
        tmp.push(msg.sender);
        Request memory requestInstance = Request(n, _recipient, _amount, false, tmp);
        requests.push(requestInstance);
    }
    
    function viewTransferRequests() public view returns(Request[] memory){
        return requests;
    }
    
    function approveTransfer(uint _id) public onlyOwners{
        require(!requests[_id].accepted, "This transaction has already been completed!");
        bool firstVote = true;
        uint i=0;
        for(;i<requests[_id].approvers.length;i++){
            if(msg.sender == requests[_id].approvers[i]){
                firstVote = false;
            }
        }
        require(firstVote, "You already accepted this request!");
        emit ApprovalRecieved(_id, msg.sender, i+1);
        requests[_id].approvers.push(msg.sender);
        if(i + 1 >= minApprovals){
            transfer(_id);
        }
    }
    
    function transfer(uint _id) private{
        require(balance >= requests[_id].amount, "Balance not sufficient");
        emit TransferApproved(_id);
        requests[_id].accepted = true;
        balance -= requests[_id].amount;
        payable(requests[_id].recipient).transfer(requests[_id].amount);
    }
    
    function getBalance() public view onlyOwners returns(uint){
        return balance;
    }
    
    function viewOwners() public view returns(address[] memory){
        return owners; 
    }
}

1 Like

Hello Everyone!

So I am ready with this assignment ( needed around 20% help but I took a long time to understand everything in it).

There is one more thing that I would like to add. So on Philip’s video, he has a deposit function when anybody can deposit to the multisig wallet. I would like to make a view return statement to actually see how much balance/amount of wei the Multisig Wallet has. Any idea how to return its amount? The deposit function is just the following:

function deposit() public payable {}

Thanks!

1 Like

Hello Guys! So here is my full project code. I have needed assistance in the modifier section and a bit on the approve section but in 80% I was fine. I have added some more things, like the getbalance of the transaction and also for the wallets itself ( not sure about the wallet part yet, have a question in this thread). I have decided to run with Philip’s variables ( was easier to understand as I overthought it in the first place). Now I am going to create something on my own coming from Philip’s suggestion at the end of the course, post it here and then move to Solidity 201.

pragma solidity 0.7.5;
pragma abicoder v2;


contract Multisig {
    uint depositedEther;
    address[] public owners;
    uint limit;
    
  
    mapping(address => mapping(uint => bool)) approvals;
    // [address] > [transaction ID] > [yes/no]
    
    struct Transfer {
        address payable recipient;
        uint approval;
        bool hasBeenSent;
        uint amount;
        uint id;
        
    }
    
    event transferCreated(address payable recipient, uint amountSent, uint transactionID);
    event transferApproved(uint transactionIDApproved, address approvalAddress);
    
    
    Transfer[] transferRequests;
    
    constructor(address[] memory _owners, uint _limit){
        owners = _owners;
        limit = _limit;
    }
    
    modifier onlyOwners() {
        bool owner = false;
        for (uint i = 0; i < owners.length; i++){
            if(owners[i] == msg.sender){
                owner = true;
                
            }
        }
        require(owner == true);
        _;
    }
    
    function deposit(uint _depositedEther) public payable {
     depositedEther = _depositedEther;
    
    }
    
    function createTransfer(address payable _recipient, uint _amount) public onlyOwners {
        transferRequests.push(Transfer(_recipient, 0, false, _amount, transferRequests.length));
        emit transferCreated(_recipient, _amount, 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].approval++;  
             emit transferApproved(_id, msg.sender);
        
        if(transferRequests[_id].approval >= limit){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].recipient.transfer(transferRequests[_id].amount);
            
        }
        
        
    }
    function getAllTransaction() public view returns(Transfer[] memory){
        return transferRequests;
    }
    function getBalanceOfTransactionID(uint _id) public view returns (uint){
        return transferRequests[_id].amount;
    }
    function getWalletBalance() public view returns(uint){
    return depositedEther;
    }
}
1 Like

Nice solution @Riki, congratulations.

Now you can have something like this to return the balance of the contract:

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

Carlos Z

1 Like