Project - Multisig Wallet

Here is my project code. full disclosure I did do a course on this before I signed up with moralis. so I coded what I could with the understanding from this more informed moralis course and did a cross reference with this contract I had coded before.
pragma experimental ABIEncoderV2;

contract Wallet {

address[] public approvers;

uint public quorum; // quorum is number of approver needed to approve a transfer

struct Transfer {

    uint id;

    uint amount;

    address payable to;

    uint approvals;

    bool sent;

}

Transfer[] public transfers;

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

constructor(address[] memory _approvers, uint _quorum) public {

    approvers = _approvers;

    quorum = _quorum;

}

function getApprovers() external view returns (address[]  memory){

    return approvers;

}

function getTransfers() external view returns (Transfer[]  memory){

    return transfers;

}

function createTransfer(uint amount, address payable to) external onlyApprover {

transfers.push(Transfer(

  transfers.length,

  amount,

  to,

  0,

  false

));

}

function approveTransfer(uint id) external onlyApprover {

    require(transfers[id].sent == false, 'transfer has already been sent');

    require(approvals[msg.sender][id] == false, 'cannot approve transfer twice');

    approvals[msg.sender][id] = true;

    transfers[id].approvals++;

    if(transfers[id].approvals >= quorum){

        transfers[id].sent = true;

        address payable to = transfers[id].to;

        uint amount = transfers[id].amount;

        to.transfer(amount);

    }

}

receive() external payable {}

modifier onlyApprover() {

    bool allowed = false;

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

        if(approvers[i] == msg.sender){

            allowed = true;

        }

    }

    require(allowed == true, 'only approver allowed');

    _;

}

}

2 Likes

It could be more dynamic but I thing it follow the requirements.

// SPDX-License-Identifier: UNLICENCED
pragma solidity 0.7.5;
pragma abicoder v2;


contract Ownable {
    address[] owners;

    modifier onlyOwners {
        bool found;
        for (uint i=0; i < owners.length; i++) {
            if (msg.sender == owners[i]) found = true;
        }
        require(found, "Only owners!");
        _;
    }

    constructor(address[] memory _owners) {
        owners = _owners;
    }
}

contract MultisigWallet is Ownable {

    uint public approvalLimit;

    struct Request {
        uint256 amount;
        address to;
        address from;
        uint    approvals;
        bool    sent;
    }
    Request[] request;


    // store every approved request by owners
    mapping (address => uint[]) approved;

    constructor(address[] memory _owners, uint _approvalLimit) Ownable(_owners) {
        require(_owners.length > 0, "Need to specify owners!");
        // Approval limit should be at max total owners - 1! Request owner cannot approve its own request!
        require(_owners.length > _approvalLimit, "Approval limit will never be reach! because it is higher than total owners!");
        require(_approvalLimit > 0, "Approval should be higher than 0");
        approvalLimit = _approvalLimit;
    }

    //Anyone should be able to deposit ether into the smart contract
    //Could be use -> receive() external payable {}
    function deposit() public payable {}

    //Anyone of the owners should be able to create a transfer request
    function requestTransfer(uint256 _amount, address _to) public onlyOwners returns(uint id){
        Request memory newRequest;
        newRequest.amount = _amount;
        newRequest.to = _to;
        newRequest.from = msg.sender;
        request.push(newRequest);
        return request.length;
    }

    // Retreive a request
    function getRequest(uint _id) public view returns (Request memory){
        require(_id <= request.length, "Id not found!");
        return request[_id];
    }
    
    //Owners should be able to approve transfer requests
    //Return if request has been sent or not
    function approve(uint _id) public onlyOwners returns (bool){
        require(_id < request.length, "Id not found!");
        require(request[_id].from != msg.sender, "You cannot approve your own request!");
        if (approved[msg.sender].length > 0) {
            for (uint i = 0; i < approved[msg.sender].length; i++){
                require(approved[msg.sender][i] != _id, "You already approved this request!"); // To test the loop, you need owners.length >= approvalLimit + 2
            }
        }
        require(!request[_id].sent, "Request has already been approved!");
        
        approved[msg.sender].push(_id);
        request[_id].approvals++;

        // No need to put >= instead of == BUT it could be more safety ???
        if (request[_id].approvals == approvalLimit){
            require(address(this).balance > request[_id].amount, "Not enough balance for the request amount!");
            address payable _to = payable(request[_id].to);
            _to.transfer(request[_id].amount);
            request[_id].sent = true;
        }
        return request[_id].sent;

    }

}
1 Like

Is my way of doing it OK? Any comments on it from Moralis educators? Pros, cons…etc

1 Like

yep @Tomaage. tour solution looks perfect, i would suggest removing the payable from the addresses in the constructor. you dont need this. only specificy the payable warpper if you want to send ether specifically to an address this is the only time u would use this. so wrapping it around the addresses in the constructor is not needed. other than this ur sol lok svery good. well done. onto to 201 now

MultisigWallet.sol
pragma solidity 0.7.5;

contract Multisig {
    
    mapping(address => bool) owners;
    uint requiredNumOfApprovals;

    constructor(){
        owners[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = true;
        owners[0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db] = true;
        owners[0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2] = true;
        requiredNumOfApprovals = 2;
    }

    event depositDone(uint amount, address indexed depositedFrom);
    event transferRequested(address indexed requestedBy, address indexed recipient, uint amount, uint indexed txId, uint Approvals);
    event transactionSent(address indexed to, uint amount, uint indexed txId);
    event transactionUpdated(uint txId, uint Approvals);
    
    modifier onlyOwners {
        require(owners[msg.sender] == true, "You are not the owner");
         _;
    }
    
    struct transaction {
        address payable to;
        uint amount;
        uint txId;
        uint numOfApprovals;
        bool transactionSent;
        bool owner1app;
        bool owner2app;
        bool owner3app;
    }

    transaction[] transactionLog;

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

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

    function requestTransfer(address payable _recipient, uint _amount) public onlyOwners {
        require(address(this).balance >= _amount, "Balance not sufficient");
        transactionLog.push( transaction(_recipient, _amount, transactionLog.length, 0, false, false, false, false) );
        emit transferRequested(msg.sender, _recipient, _amount, transactionLog.length-1, 0);
    } 

    function approveTransaction(uint _txId) public onlyOwners {
        if(msg.sender == 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 && transactionLog[_txId].owner1app == true )
            {require(transactionLog[_txId].owner1app != true, "You already approved this transaction."); }
        if(msg.sender == 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db && transactionLog[_txId].owner2app == true )
            {require(transactionLog[_txId].owner2app != true, "You already approved this transaction."); }
        if(msg.sender == 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 && transactionLog[_txId].owner3app == true )
            {require(transactionLog[_txId].owner3app != true, "You already approved this transaction."); }
       
   
        transactionLog[_txId].numOfApprovals += 1;
        if(msg.sender == 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4){
            transactionLog[_txId].owner1app = true;
        }
        if(msg.sender == 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db){
            transactionLog[_txId].owner2app = true;
        }
        if(msg.sender == 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2){
            transactionLog[_txId].owner3app = true;
        }
        emit transactionUpdated(transactionLog[_txId].txId, transactionLog[_txId].numOfApprovals);
    }

    function sendTransaction(uint _txId) public onlyOwners {
        require(transactionLog[_txId].transactionSent == false, "Transaction alredy sent.");
        require(transactionLog[_txId].numOfApprovals >= requiredNumOfApprovals, "Not approved you need 2 or more approvals");
        require(address(this).balance >= transactionLog[_txId].amount, "Balance not sufficient");
        transactionLog[_txId].to.transfer(transactionLog[_txId].amount);
        transactionLog[_txId].transactionSent = true;
        emit transactionSent(transactionLog[_txId].to, transactionLog[_txId].amount, transactionLog[_txId].txId);
    }

}

If I remove the payable I get error messages. Wonder if it has to do woth the pragma version I use.

Some genereal feedback on these points as well would be nice. @jon_m and if Jon is not on this anymore, is there sombody elese I could ask @thecil ? My study guide said I could ask you .

  1. How is it gas wise that I put the addresses for owners directly in the constructor? Any difference sine I dont need to use a “for” loop to look for the owners in the modifier.

  2. Is it possible to remove the constructor here, and have my owner adresses directly in the state varibles? Would it be drawbacks to doing that? Or any cons?

  3. Are there any major gas implications with the way I have done it?

  4. IS it recommended that you use a constructor to initiate state variables that will not change after you deploy contract?

2 Likes

@Tomaage, passing in the addresses in the constructor is fine and ask you say will save you on gas. however tou could aslo hardcode the addressess into the contract also. the reason i wouldnt recommend doing it this way is necause if u hardcode the addresses then each time you deploy the contract the woners will be the same. whwereas if you have the addresses as constants that get defined on contract creation, then you can have unique instances of this wallet with different owners. for example some random person could come and take your code and use it for himself. so hardcoding the addresses is a bad way to do it in terms of versailltity.

And yes usually you do use the constructor to initalise state variables (not all) tho. some variabled that are just going to be constant or the same no matther what the scenario, like the address of a token for example will always b the same. other than this yes your right you should pust state variables into the constructor so your contract can be more versatile. however this doesnt mean tat you need a constricor for every contract. it all depends on ehat your goal is.

and in terms of gas it is fine you dont have any for loops so your good for gas. there are tricks you will learn in the next course solddity 201 which will teach you about gas optimization even better

My complete source code is on github.

The main difference from the official solution is that I used a different data structure for holding the pending transfers and their approvals:

transferRequests[recipientAddress][amount][approverAddress] => bool 
where bool is true if approverAddress has approved the transaction,
or false, otherwise.

Other main difference is that there is a single method for proposing and/or approving a transfer.
So, the creator calls “approveTransfer” for creating the transaction and the other owners call the same method for approving it. Once it has enough approvals, the transfer is made.

@filip and others, would love to hear your feedback! :slight_smile:

1 Like

Hello this is my solution:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
pragma abicoder v2;
import “@openzeppelin/contracts/utils/Counters.sol”;
contract Wallet{
using Counters for Counters.Counter;

address[] public owners;
uint approversRequired;
Counters.Counter private _txIds; ///_itemsSold.increment();   uint256

mapping(address => mapping(uint256 => bool)) private ownerTXApproveMapping;
mapping(uint256 => TransactionRequest) private transactionMapping;

modifier onlyOwner() {
    bool isOwner = false;

    for (uint i=0; i < owners.length; i++) {
        if (msg.sender == owners[i]) {
            isOwner = true;
        }
    } 
    require(isOwner, "Caller is not owner");
    _;
}

struct TransactionRequest{
    uint approvers;
    address requester;
    uint amount;
    uint256 txId;
    string status;     
}


event TransactionRequestCreated(address, uint, uint256);
event TransactionRequestApproved(address, uint);
event TransactionRequestSubmited(address, uint);

constructor (address[] memory _owners, uint _approversRequired){
    owners = _owners;
    approversRequired = _approversRequired;
}

function createTransactionRequest(uint _value) public onlyOwner returns(bool){
    TransactionRequest memory request  = TransactionRequest (0, msg.sender, _value, _txIds.current(), "CREATED");
    transactionMapping[_txIds.current()] =  request;  
    emit TransactionRequestCreated(msg.sender, _value, _txIds.current());
    _txIds.increment();  
    return true;
}

function approveTransactionRequest(uint256 _idTx) public onlyOwner returns(bool){
    require(ownerTXApproveMapping[msg.sender][_idTx]==false, "Owner already approved");
    require(transactionMapping[_idTx].approvers < approversRequired , "transaction already submited");
    require(transactionMapping[_idTx].requester != msg.sender , "approver can't be the creator");
    ownerTXApproveMapping[msg.sender][_idTx]=true;
    transactionMapping[_idTx].approvers = transactionMapping[_idTx].approvers + 1;
    transactionMapping[_idTx].status="APPROVED";
    if (transactionMapping[_idTx].approvers == approversRequired){
        transactionMapping[_idTx].status="SUBMITED";
        emit TransactionRequestSubmited(msg.sender, _idTx);
    } else {
        emit TransactionRequestApproved(msg.sender, _idTx);
    }
    
    
    return true;
}

}

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    address[] public owners;
    uint limit;
    
    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }

    event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _receiver);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approvals;
    
    //Should only allow people in the owners list to continue the execution.
    modifier onlyOwners(){
        bool owner = false;
        for(uint i = 0; i<owners.length; i++){
            if(owners[i] == msg.sender){
                owner = true;
            }
        }
        require(owner == true);
        _;
    }
    //Should initialize the owners list and the limit 
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    //Empty function
    function deposit() public payable {}
    
    //Create an instance of the Transfer struct and add it to the transferRequests array
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);
        transferRequests.push(
             
        Transfer(_amount, _receiver, 0 , false, transferRequests.length)
        );
       
    }
    
    //Set your approval for one of the transfer requests.
    //Need to update the Transfer object.
    //Need to update the mapping to record the approval for the msg.sender.
    //When the amount of approvals for a transfer has reached the limit, this function should send the transfer to the recipient.
    
    function approve(uint _id) public onlyOwners {

    //An owner should not be able to vote twice.
    //An owner should not be able to vote on a tranfer request that has already been sent.
        require(approvals[msg.sender][_id] == false);
        require(transferRequests[_id].hasBeenSent == false);

        

        approvals[msg.sender][_id] = true;
        transferRequests[_id].approvals++;

emit ApprovalReceived(_id, transferRequests[_id].approvals, msg.sender);


        if(transferRequests[_id].approvals >= limit){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
            TransferApproved(_id);
        }
    }
    
    //Should return all transfer requests
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    }
    
    
}
1 Like

Multisig Wallet Contract

pragma solidity ^0.8.0;
pragma abicoder v2;
import "./SafeMath.sol";

contract MultiSigWallet {

    uint contractBalance;
    address[] public owners;
    uint numOfApprovalsRequired;
    
    struct TransferRequest {
	    uint index;
	    address payable receiver;
	    uint amount;
	    uint countApproved;
	    bool sent;
    }

    constructor(address[] memory _owners) {
        owners = _owners;
        numOfApprovalsRequired = owners.length - 1;
    }

    modifier onlyOwners(){
        
        uint i;
        bool isOwner = false;
        
        for (i=0; i < owners.length; i++){
            if(owners[i] == msg.sender) {
                isOwner = true;
                break;
            }
        }
   
        require(isOwner, "This function is restricted to Contract Owners");
        _; //Continue execution
    }

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

    TransferRequest [] public allTransferRequests;

    event transferFunds(uint, address);
    event depositedToContract(uint, address);

    //anyone can deposit ether to the contract
    function deposit() public payable {
	    
	    contractBalance = SafeMath.add(contractBalance,msg.value);
		emit depositedToContract(msg.value, msg.sender);
    }

    //anyone of the owners can initiate a transfer request - amount and to what address
    //transfer occurs when number of approvals is met

    function initiateTransfer(address payable _recipient, uint _amount) public payable onlyOwners{
	    
	    require(address(msg.sender).balance >= _amount, "Balance not sufficient");
	    require(msg.sender != _recipient, "Don't transfer money to yourself");

		//create new transaction and store in array
		TransferRequest memory newTransferRequest;
        
        newTransferRequest.index = allTransferRequests.length;
        newTransferRequest.receiver = _recipient;
        newTransferRequest.amount = _amount;
        newTransferRequest.countApproved = 0;
        newTransferRequest.sent = false;
  
        createNewTransferRequest(newTransferRequest);
    }

	function createNewTransferRequest(TransferRequest memory _newRequest) public {
	    allTransferRequests.push(_newRequest);
	}
	
    function getContractBalance() public view returns (uint) {
        return address(this).balance;
    }
    
    function getRequestDetails(uint index) public view returns (address recipient, uint amount, uint timesApproved, bool sent){
        return (allTransferRequests[index].receiver, allTransferRequests[index].amount, allTransferRequests[index].countApproved, allTransferRequests[index].sent);
    }
    
    function approveTransaction(uint index) public onlyOwners {
  
	    //An owner should not be able to vote twice //An owner should not be able to vote on a tranfer request that has already been sent.
	    
	    if ((approvals[msg.sender][index] != true) && (allTransferRequests[index].sent == false)) {
	        
	        //Set your approval for one of the transfer requests.
	        allTransferRequests[index].countApproved++; //Need to update the Transfer object.
	    
	        approvals[msg.sender][index] = true;        //Need to update the mapping to record the approval for the msg.sender.	        
	    }

	    //When the amount of approvals for a transfer has reached the limit, this function should send the transfer to the recipient.
	    
	    if ((allTransferRequests[index].countApproved >= numOfApprovalsRequired) && (allTransferRequests[index].sent == false)){
	        
	        uint previousContractBalance = address(this).balance;
		    contractBalance = SafeMath.sub(previousContractBalance, allTransferRequests[index].amount);
		    
		    allTransferRequests[index].receiver.transfer(allTransferRequests[index].amount);

		    assert(address(this).balance == contractBalance);
		    
		    emit transferFunds(allTransferRequests[index].amount, allTransferRequests[index].receiver);
		    allTransferRequests[index].sent = true;
	    }
	}
	
	function getTransferRequests() public view returns (TransferRequest[] memory){
	    return allTransferRequests;
    }
}
1 Like

Great implementation! really good code and logic.

However doesn’t it defeat the purpose that a single member(CEO) can change the number of members in the board?

for an example of this is a defi project with a board and this is the multi sig wallet with the revenue generated/locked capital, if I am the CEO for me to empty the vault to my own address what I can do is remove every board member so that I can sign the transaction myself?

(your skills are great btw I’m still trying to write my code and I am struggling with the dynamic part myself)

1 Like

Euhm, sure, from an application perspective I guess that would make more sense. However, the scope of this assignment is from a technical and implementation perspective, with a focus on the basics of Solidity :wink:

An implementation like you propose seems a bit more advanced and on a higher level of usability then simple programming some Solidity to understand the workings of contracts :slight_smile: However, don’t let me stop you from trying to implement it!

2 Likes

I was having trouble trying to figure out some of the tasks on my own so I used the video help to code out the template. I did make some adjustments to the final code. I added a getMyBalance function for individual address balances, added a totalBalance function for the overall contract balance, and also removed “public” from the 5th line at the top for “address[] owners;” to remove the function box when the contract is deployed. It cleaned it up and everything still worked fine from what I saw.

Please let me know if I need to correct anything.

pragma solidity 0.7.5;
pragma abicoder v2;

contract Wallet {
    address[] owners;
    uint limit;
    
    struct Transfer{
        uint amount;
        address payable receiver;
        uint approvals;
        bool hasBeenSent;
        uint id;
    }

    event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _receiver);
    event ApprovalReceived(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint => bool)) approvals;

    mapping(address => uint) balance;
    
    //Should only allow people in the owners list to continue the execution.
    modifier onlyOwners(){
        bool owner = false;
        for(uint i=0; i<owners.length;i++){
            if(owners[i] == msg.sender){
                owner = true;
            }
        }
        require(owner == true);
        _;
    }
    //Should initialize the owners list and the limit 
    constructor(address[] memory _owners, uint _limit) {
        owners = _owners;
        limit = _limit;
    }
    
    function deposit() public payable returns (uint) {
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }

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

    function totalBalance() public view returns (uint){
        return address(this).balance;
    }
    
    //Create an instance of the Transfer struct and add it to the transferRequests array
    function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);
        transferRequests.push(
            Transfer(_amount, _receiver, 0, false, transferRequests.length)
        );
        
    }
    
    //Set your approval for one of the transfer requests.
    //Need to update the Transfer object.
    //Need to update the mapping to record the approval for the msg.sender.
    //When the amount of approvals for a transfer has reached the limit, this function should send the transfer to the recipient.
    //An owner should not be able to vote twice.
    //An owner should not be able to vote on a tranfer request that has already been sent.
    function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] == false);
        require(transferRequests[_id].hasBeenSent == false);

        approvals[msg.sender][_id] = true;
        transferRequests[_id].approvals++;

        emit ApprovalReceived(_id, transferRequests[_id].approvals, msg.sender);

        if(transferRequests[_id].approvals >= limit){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
            emit TransferApproved(_id);
        }
    }
    
    //Should return all transfer requests
    function getTransferRequests() public view returns (Transfer[] memory){
        return transferRequests;
    }
    
    
}
1 Like
pragma solidity 0.7.5;
pragma abicoder v2;

contract Multisig {

    mapping(address => bool) owner;
    
    Transfer[] transferRequests; //stores all Transfer requests
    
    uint limit;
    
    mapping(address => uint) balance;

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

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

    event newTransferRequested(uint _amount, uint _txId, address _to, address _sender);
    event approvalAdded(uint _txId, address _approver);
    event TransferApproved(uint _txId);

    //transfer request requires
    struct Transfer {
        address payable to;
        uint amount;
        uint approvals;
        bool wasSent;
        uint txId;
    }

    constructor(address _ownerB, address _ownerC, uint _limit) {
        owner[msg.sender] = true;
        owner[_ownerB] = true;
        owner[_ownerC] = true;
        limit = _limit;
    }
    
    function deposit() public payable returns(uint) {
        balance[msg.sender] += msg.value;
        return balance[msg.sender];
    }

    function approveTransfer(uint _txId ) public onlyOwners {
        require(approvals[msg.sender][_txId] == false);
        require(transferRequests[_txId].wasSent == false);
        
        approvals[msg.sender][_txId] = true;
        transferRequests[_txId].approvals++;

        emit approvalAdded(_txId, msg.sender);

        if(transferRequests[_txId].approvals >= limit){
            transferRequests[_txId].wasSent = true;
            transferRequests[_txId].to.transfer(transferRequests[_txId].amount);
            emit TransferApproved(_txId);
        }
    }

    //creates transfer
    function transferRequest(address payable _to, uint _amount) public onlyOwners {
        emit newTransferRequested(_amount, transferRequests.length, _to, msg.sender);
    
        transferRequests.push(Transfer(_to, _amount, 0, false, transferRequests.length));
    }

     function getTransferInfo() public view returns (Transfer[] memory) {
        return transferRequests;
    }
}
2 Likes
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;
pragma abicoder v2;

// * Wallet Requirements *

// Anyone should be able to deposit ether to the Wallet ✅

// The contract creator should be able to Input 
// (01) The addresses of the owners and ✅
// (02) The Number 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.✅

contract Wallet{

    address[] public owners;
    uint limit;

    struct transferRequest{
        address payable reciever;
        uint amount;
        uint id;
        bool sent;
        uint approvalCount;
    }
    
    transferRequest[] transferLog; 

    //double mapping required to track approvals
    mapping(address =>mapping(uint=>bool)) approvals;


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

    //declaring the owners of the wallet and the required limit for approval of transactions 
    constructor(address[] memory _owners,uint _limit){
        owners = _owners;
        limit =_limit;
    }

    //deposit ether to contract
    function deposit() public payable{
    }

    //creating the initial transfer request by the owners
    function createTransferRequest(address payable _reciever, uint _amount) public onlyOwners{
        transferLog.push(transferRequest(_reciever, _amount, transferLog.length,false,0));
    }

    //aprroving the pending requests by the owners 
    function approveTranfer(uint _id) public onlyOwners{
        require(approvals[msg.sender][_id] ==false);

        approvals[msg.sender][_id] =true;
        transferLog[_id].approvalCount++;

        if(transferLog[_id].approvalCount >= limit){
            transferLog[_id].reciever.transfer(transferLog[_id].amount);
            transferLog[_id].sent = true;
        }

    }
    
    //checking the transfer request log
    function getTranferRequest()public view returns(transferRequest[] memory){
        return transferLog;
    }
    //checking the contract balance
    function getContractBalance()public view returns(uint){
        return address(this).balance;
    }

}
1 Like

This assignment had to sink in for a while. First I made my own contract and got horrible stuck, because my set-up wasn’t a proper framework to work with.

So I did some research with Google and Filip’s videos. Then made multiple versions (not all at once). Further I changed names of variables, arguments, functions and events a lot to challenge myself and get comfortable with the code (hence the unusual names :)). And I added the function getWalletFriendsBalance() to easily check the results of deposits and transactions and to check that the transaction to be submitted doesn’t exceed the balance.

The best part was testing the contract in Remix. It is awesome to see the wallet in action, make mistakes, do some corrections, try again and finally see all code working as expected. The data field was confusing though. But after I filled in the default setting 0x00 (because while testing transactions in this contract there is no need yet to call other contracts and store bytes) all worked well.

If you’re still reading this and looking into my solution, thank you! And if you happen to have questions or comments, please post them and thank you very much again!

pragma solidity 0.8.13;
pragma abicoder v2;

contract WalletFriends {
    //store the owners in an array of addresses
    address[] public owners;

    //Check if msg.sender is owner. 
    //If an address is an owner of the wallet it should return true
    mapping (address => bool) public isOwner;

    mapping(address => uint) balance;

    //store number of approvals that is required for a transaction to be executed
    uint public numApprovalsRequired;

    struct Transaction {
        address receiver;
        uint value;
        bytes data;
        bool transactionCompleted;
        uint numApprovals;
    }
   
    //store all transactions in an array
    Transaction[] public transactions;

    //make sure msg.sender is one of the owners of this contract
    modifier onlyOwner() {
        require(isOwner[msg.sender], "owner not verified");
        _;
    }

    //check if the transaction really exists
    modifier transactionExists(uint _transactionIndex){
        require(_transactionIndex < transactions.length, "transaction does not exist yet");
        _;
    }

    //check if the transaction is not yet approved by msg.sender
    modifier notApproved(uint _transactionIndex){
        require(!isApproved[_transactionIndex][msg.sender], "transaction is already approved");
        _;
    }

    //make sure the transaction is not already finished
    modifier notCompleted(uint _transactionIndex){
        require(!transactions[_transactionIndex].transactionCompleted,"transaction is already completed");
        _;
    }

    //store the approval of each transaction by each owner in a mapping
    //uint is the transaction index, address is address of the owner who's considering approval
    //bool is check wether the transaction is approved by this owner
    mapping (uint => mapping (address => bool)) public isApproved;

    event Deposit(address indexed sender, uint value, uint balance);

    event SubmitTransaction(
        address indexed owner,
        uint indexed transactionIndex,
        address indexed receiver,
        uint value,
        bytes data
    );

    event ApproveTransaction(address indexed owner, uint indexed transactionIndex);

    event Complete(uint indexed transactionIndex);

    constructor(address[] memory _owners, uint _numApprovalsRequired) {
        //require that this wallet has 3 owners
        require(_owners.length == 3, "owners needed");
        //require a correct number of approvals treshold
        require(
            _numApprovalsRequired > 0 &&
                _numApprovalsRequired <= _owners.length,
            "incorrect number of required approvals"
        );

        //save owners to a state variable
        //make sure that owner is not equal to the address on index 0
        //check if the owner is unique
        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];

            require(owner != address(0), "unfounded owner");
            //verify that the new owner is not yet in our mapping isOwner
            require(!isOwner[owner], "owner not unique");

            //insert the new owner in the isOwner mapping
            isOwner[owner] = true;
            //add the owner into the owners state variable
            owners.push(owner);
        }

        //set state variable numApprovalsRequired equal to the _numApprovalsRequired from the input
        numApprovalsRequired = _numApprovalsRequired;
    }

   //anyone can deposit
   function deposit() public payable returns(uint){
    balance[msg.sender] += msg.value;
    emit Deposit(msg.sender, msg.value, address(this).balance);
    return balance [msg.sender];
  }
   
  //get total balance of this wallet contract, to keep track of how much Eth you can actually transfer 
  function getWalletFriendsBalance() public view returns (uint){
        return address(this).balance;
    }

    //only owners can submit a transaction
    function submitTransaction(address _receiver, uint _value, bytes memory _data) public onlyOwner {
        uint transactionIndex = transactions.length;

        //push parameters into the transaction array
        //set additional information on transaction status
        transactions.push(Transaction({
            receiver: _receiver,
            value: _value,
            data: _data,
            transactionCompleted: false,
            numApprovals: 0})
            );
        
        //log function data with the event SubmitTransaction
        emit SubmitTransaction(msg.sender, transactionIndex, _receiver, _value, _data);

    }

    //after a transaction is submitted other owners can approve the transaction
    function approveTransaction(uint _transactionIndex) external onlyOwner {

        require(_transactionIndex < transactions.length, "transaction does not exist yet");
        require(!transactions[_transactionIndex].transactionCompleted,"transaction is already completed");
        require(!isApproved[_transactionIndex][msg.sender], "transaction is already approved");

        //store the transaction approval of msg.sender
        Transaction storage transaction = transactions[_transactionIndex];
        transaction.numApprovals += 1;
        isApproved [_transactionIndex][msg.sender] = true;

        emit ApproveTransaction(msg.sender, _transactionIndex);
    }

    //make sure that the number of approvals is equal to or greater than the required numbers of approvals
    //count the number of approvals with a function
    function _countApprovals(uint _transactionIndex) private view returns(uint count) {
        for (uint i; i < owners.length; i++){
            if (isApproved[_transactionIndex][owners[i]]){
                count +=1;
            }
        }
    }

    //execute the transaction
    function completeTransaction(uint _transactionIndex) external {

            require(_transactionIndex < transactions.length, "transaction does not exist yet");
            require(!transactions[_transactionIndex].transactionCompleted,"transaction is already completed");

            //check that the count of approvals is >= than required
            require(_countApprovals(_transactionIndex)>= numApprovalsRequired,"approvals is less than required");
            
            //get the data stored in the Transaction struct and then update it
            Transaction storage transaction = transactions[_transactionIndex];

            transaction.transactionCompleted = true;

            //execute the transaction
            (bool success, ) = transaction.receiver.call{value: transaction.value}(
            transaction.data
        );

            require(success, "transaction error");

            emit Complete(_transactionIndex);
        }

}
1 Like

I thought about using a dynamic approval array in my struct to signify who had made an approval. Further investigation indicated this is either not possible or not a good practice, can some one possibly comment on this?

anyways my solution code:

pragma solidity 0.7.5;
pragma abicoder v2;

import "./Destroyable.sol";
import "./Ownable.sol";

contract MultiSigWallet is Destroyable {
    struct TransferRequest{
        uint amount;
        uint noOfApprovals;
        address payable recipient;
        bool sent;
    }

    uint minNoOfSignaturesToApprove;
    TransferRequest[] transferRequests;
    mapping(address => mapping(uint => bool)) approvals;

    event TransferRequestCreated(uint indexed transferRequestId, address indexed recipient, uint amount, address initiatedBy);
    event TransferRequestApproved(uint indexed transferRequestId, address indexed approver);
    event TransferRequestSent(uint indexed transferRequestId);

    constructor(address[] memory _owners, uint _minNoOfSignaturesToApprove) {
        minNoOfSignaturesToApprove=_minNoOfSignaturesToApprove;
        owners = _owners;
    }

    function deposit() public payable returns (uint) {
        if (!ownerExists(msg.sender)) {
            owners.push(msg.sender);
        }

        return address(this).balance;
    }

    function makeTransferRequest(address payable recipient, uint amount) public onlyOwners {
        requireAmountLessThanBalance(amount);

        emit TransferRequestCreated(transferRequests.length, recipient, amount, msg.sender);
        approvals[msg.sender][transferRequests.length] = true;
        transferRequests.push(TransferRequest(amount,1,recipient,false));        
    }

    function approveTransferRequest(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id]==false, "Sender has already approved the transfer");
        require(transferRequests[_id].sent == false, "The transfer has already been sent");
  
        approvals[msg.sender][_id] = true;
        transferRequests[_id].noOfApprovals++;
        
        if(transferRequests[_id].noOfApprovals >= minNoOfSignaturesToApprove) {
            requireAmountLessThanBalance(transferRequests[_id].amount);

            transferRequests[_id].recipient.transfer(transferRequests[_id].amount);
            transferRequests[_id].sent = true;

            emit TransferRequestSent(_id);
        }
        emit TransferRequestApproved(_id, msg.sender);
    }

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

    function requireAmountLessThanBalance(uint amount) internal view {
        require(address(this).balance >= amount, "An amount greater than the balance cannot be sent");
    }

    function getTransferRequests() public view returns (TransferRequest[] memory){
        return transferRequests;
    }
}
2 Likes
pragma solidity 0.7.5;
pragma abicoder v2;


contract Wallet {
    address[] public owners;
    uint minApproval;


    Transfer[] transferReq;
    mapping(address => mapping(uint => bool)) approvals;

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

    modifier onlyOwners(){
        bool owner = false;

        for(uint i = 0; i < owners.length; i++){
            if(owners[i] == msg.sender){
                owner = true;
            }
        }
        require(owner == true, "Not Owner");
        _;
    }


    constructor(address[] memory _owners, uint _minApproval){
        owners = _owners;
        minApproval = _minApproval;
    }

    function deposit() public payable{
        //Keep empty
    }

    function transfer(uint _amount, address payable _receiver) public onlyOwners{
        transferReq.push( Transfer(_amount, _receiver, 0, false, transferReq.length) );
    }

    function approve(uint _id) public onlyOwners{
        require(approvals[msg.sender][_id] == false, "Already approved");
        require(transferReq[_id].isSent == false, "Transaction already sent");

        approvals[msg.sender][_id] = true;
        transferReq[_id].approvals++;

        if(transferReq[_id].approvals >= minApproval){
            transferReq[_id].receiver.transfer(transferReq[_id].amount);
            transferReq[_id].isSent = true;
        }
    }

    function getTransferRequests() public view returns (Transfer[] memory){
        return transferReq;
    }

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

2 Likes

nice this is a cool one. tripple mappings are quite uncommon but its cool to see this approach. great work had a look at your github. keep making commits my g and soon youll have all them green squares filled