Project - Multisig Wallet

@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

it is possible, its just difficult to implement and can have weird side effects when your trying to update items in your struct. i have written a solution that does this before in this forum ill see if i can find it. just a heads up though the mapping is a beter approach. you could even change it if you like to map from a txId to an array of the approved users if thats more what your after.

1 Like

great work that you go tthere in the end. this certainly is a challenging capstone project for a beginner solidity course. your final solution look spretty good too keep up the good work. one thing i notice is you have a couple of modifiers. you should only ever place code and abstract it into its own modifier if you use that code more than once in your contrct. if a piece of logic is only being used once theres no need to wrap it inside a modifier.

1 Like

Here is my solution, I started with the basic requests then I thought, if I really have the basics I must be able to do something more so I added details like having a table that updates when a transaction is valid or not.
If it’s possible to have a feedback on the work it would be great to note important points like gas consumption, or functions that could be simplified easily etc.

Anyway, thanks to all of you for this introduction to solidity, I had a lot of fun and I could also see another side of ethereum which is coding and I love it, I learned a lot thanks to you

> // SPDX-License-Identifier: UNLICENSED
> 
> pragma solidity 0.7.5;
> 
> pragma abicoder v2;
> 
> /*------------------------description-------------------------
> 
>  This is a multi-sig portfolio, at the beginning you can choose how many owners you want.
> 
>  and you must define the number of approvals needed to validate a transaction
> 
>  After that you can deposit ETH, owner or not
> 
>  Only owner can request a transation and only onwers can validate it
> 
>  You can see the currentwaiting list by calling SeePendingTransaction
> 
>  You can see the current waiting list by calling SeePendingTransaction.
> 
>  You can see the trx refuse list by calling SeeVAnnulationTransaction
> 
>  You can see owners of this folio by calling SeeOwner
> 
>  You can see amount of this SC  by calling ETHpool
> 
> */
> 
> //------------------------storage-declaration----------------------
> 
> contract multiSigWallet{

>     address  [] owner;
>     uint nbValidation;

>     mapping(address => mapping(uint=> bool)) vote;
>     mapping (address => uint)balance;
>     mapping (address => bool)answer; // need to use false in entry to say no otherwise remix take true
> 
>    
>     struct transaction{
> 
>         uint txId;
>         uint validations;
>         address payable to;
>         uint amount;
>         uint pending; // 0 invalidate // 1 accepted// 2 // refuse
>     }
> 
>     transaction []  newTransaction;
>     transaction []  transactionEnd;
> 
> //
> //------------------------event-declaration-------------------------
> 
>   event _transferRequest(address sender, address payable indexed deposited, uint indexed amount);
>   event _addTransaction(address payable indexed deposited, uint indexed amount,uint pending);
> 
> //
> //------------------------Main-part---------------------------------

>     constructor(address [] memory _owner, uint _nbValidation){
>                                                                   // check if multiple owners are the same , if it's case, It returns an error
>      address [] memory onlyOne = _owner;                         // we will make  copy of the owners array and verify each position if a owner is 2x times in the array
>      uint  check;                                               // In case of each owner is unique we validated the array owners and the nb of validations needed for a transaction
> 
>     /*---check owner array---*/
> 
>      for(uint i; i< _owner.length; i++){
>         for(uint j; j< _owner.length; j++){
>            if(onlyOne[i] == _owner[j]) check+=1;
>            }
>         }
>     /*---validations---*/
> 
>      require(check == _owner.length,"error mutliple same owner");
>       owner = _owner;
>       require(_nbValidation <= owner.length,"exceed amount of owners");
>       nbValidation = _nbValidation;
> 
>     }  
> 
>     modifier onlyOwner{
> 
>       address ownable;               // Create a modifier for being sure function who dispose of this "OnlyOwner" will be accesible only for the owners wallet
>         for(uint i =0; i < owner.length; i++){
>            if(msg.sender == owner[i]) ownable = msg.sender;
>         }
>         require(msg.sender == ownable,"You are not an owner");
>         _;
>     }
> 
>     function transferRequest(address payable _to, uint _amount) onlyOwner public{
>                                                                                   // This function will add a transaction requested by one of owners wallet
>       require(balance[address(this)] >= _amount,"not enough money");             // This function takes 2 parameters(address to send and the amount)                                                                            //Checks if the amount is available for this request and take back the potential amount from the contrat
>       balance[address(this)] -=_amount;                                         // Add the transaction to the "wainting list of transaction who waiting for validation"
>       newTransaction.push(transaction(newTransaction.length,0,_to,_amount,0));
>       emit _transferRequest(msg.sender,_to,_amount);
> 
>     }
> 
>      
> 
>     function validationTransaction(uint _index, bool _answer)public  onlyOwner{
> 
>      require(vote[msg.sender][_index] == false,"already validated");              // This function is for the validation transaction previously requested
>       require(_index < newTransaction.length,"This trx doesn't exist");          // First check if a trx is not yet already vaidated by one of wallet owners & if trx exists
>                                                                                 // If not already validate now we say thats'it by forcing "vote" to "true" and increase validations,
>       vote[msg.sender][_index] = true;                                         // to check if the nbVlidation requires are reach.
>       newTransaction[_index].validations++;                                   // take the response of the validator TRUE = YES FALSE = NO
>       answer[msg.sender] = _answer;                                          // At final check if the trx is approved else  refuse trx
>       if(newTransaction[_index].validations == nbValidation ){
>         if(trxApproved() == nbValidation) executeTransaction(newTransaction,_index);
>          else refuseTransaction(newTransaction,_index);
>             }      
>     }
> 
>     function executeTransaction( transaction [] memory,uint _index ) private{
> 
>      newTransaction[_index].to.transfer(newTransaction[_index].amount);         //This function is executed if owners validate the trx requested                                                                
>      updateTrxExecuted(_index);                                                // Execute the transaction selected by the index from the transaction requested list (SeePendingTransaction)  
>      updatePendingTransaction(_index);                                        // make an update since the trx is executed of the owner vote, the transaction requested list, -->
>      updateVote(_index);                                                     //--> the pending trx from the struct (transation)
>      emit _addTransaction(transactionEnd[_index].to,transactionEnd[_index].amount,transactionEnd[_index].pending);
> 
>     }
> 
>    
> 
>     function refuseTransaction( transaction [] memory,uint _index) private{
> 
>      balance[address(this)] += newTransaction[_index].amount;                 //This function is executed if owners refuse the trx requested
>      updateTrxRefuse(_index);                                                // Refund the SC balance with the amount from the requested trx
>      updatePendingTransaction(_index);                                      // Refuse the transaction selected by the index from the transaction requested list (SeePendingTransaction)
>      updateVote(_index);                                                   // make an update since the trx is refused of the owner vote, the transaction requested list, -->
>      emit _addTransaction(transactionEnd[_index].to,transactionEnd[_index].amount,transactionEnd[_index].pending);  //--> the pending trx from the struct (transation)
> 
>     }                                                                    
> 
>     function trxApproved()private view returns(uint){
> 
>      uint cnt;                                              //This function give us the "approval/refuse" of the owners about a requested transaction
>      for(uint i = 0; i<= owner.length-1; i++){             // Everytime this function is call, It count the number of approval transaction and return it
>                                                           // with this return we can check if the majority validate or refuse a specific trx
>         if(answer[owner[i]] == true) cnt++;
>      }
>         return cnt;
>     }
> 
>     function updatePendingTransaction(uint _index) private returns(transaction [] memory){
>                                                                                     // This function make an update of the array after a trx is validated  or refused    
>         for(uint i =_index; i<newTransaction.length-1;i++){                        //Because at the time is validated or not, this trx has been treated          
>             newTransaction[i] = newTransaction[i+1];                              // the logic behind is to copy the actual array from the trx selected and cut the last one position
>             newTransaction[i].txId =i;
>             }
>                 newTransaction.pop();
>                 return newTransaction;
>         }
> 
>     function updateTrxExecuted(uint _index) private{
> 
>      transaction memory trxSent = newTransaction[_index]; // This function update the transaction.pending to 1 because executed
>      trxSent.pending =1;
>      transactionEnd.push(trxSent);
>     }
>
>     function updateTrxRefuse(uint _index) private{
> 
>      transaction memory trxSent = newTransaction[_index]; // This function update the transaction.pending to 2 because refused
>      trxSent.pending =2;
>      transactionEnd.push(trxSent);
>     }
> 
>     function updateVote(uint _index) private{
> 
>      vote[owner[i]][_index] = false;
>     }
> 
>     function SeeValidateTransaction() public view onlyOwner returns(transaction [] memory){
> 
>      uint _index;                                                                          // This function will return all transation who are validated tracking by transation.pending
>      for(uint i=0; i< transactionEnd.length;i++){
>         if(transactionEnd[i].pending == 1) _index++;
>      }  
> 
>       transaction [] memory validateArray = new transaction[](_index);
>       _index =0;
> 
>       for(uint i=0; i< transactionEnd.length;i++){
>          if(transactionEnd[i].pending == 1) {
>             validateArray[_index] = transactionEnd[i];
>              _index++;
>          }
>       }      
>       return validateArray;
>     }    
> 
>     function SeeAnnulationTransaction() public view onlyOwner returns(transaction [] memory){
> 
>      uint _index;                                                                            // This function will return all transation who are refused tracking by transation.pending
>      for(uint i=0; i< transactionEnd.length;i++){
>         if(transactionEnd[i].pending == 2) _index++;
>      }
>
>       transaction [] memory validateArray = new transaction[](_index);
>       _index =0;
>       for(uint i=0; i< transactionEnd.length;i++){
>          if(transactionEnd[i].pending == 2) {
>             validateArray[_index] = transactionEnd[i];
>             _index++;
>          }
>       }
>        return validateArray;
>     }
> 
>     function seeOwner()public view returns(address []memory){
>         return owner;
>     }
>
>     function deposit() external payable{
>         balance[address(this)] += msg.value;
>     }
> 
>     function ETHpool() public onlyOwner  view returns(uint){
>         return balance[address(this)];
>     }
> 
>     function SeePendingTransaction() public view onlyOwner returns(transaction [] memory){
>           return newTransaction;
>     }
>  }
2 Likes
pragma solidity 0.8.7;
pragma abicoder v2;

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

    event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _reciever);
    event approvalRecieved(uint _id, uint _approvals, address _approver);
    event TransferApproved(uint _id);

    Transfer[] transferRequests;

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

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

    constructor(address[] memory _owners, uint _limit){
        require(_owners[0] != _owners[1], "the addresses must be different");
        require(_owners[1] != _owners[2], "the addresses must be different");
        owners = _owners;
        limit = _limit;
    }
    
    function deposit() public payable returns (uint)  {
    }

    function getBalance() public view onlyOwners returns(uint) {
    return msg.sender.balance;
  }



    function createTransfer(uint _amount, address payable _reciever) public onlyOwners {
        require(msg.sender.balance >= _amount, "balance must be greater than the send amount");
        emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _reciever);
        transferRequests.push(
            Transfer(_amount, _reciever, 0, false, transferRequests.length)
        );
    }

    function approve(uint _id) public onlyOwners {
        require(approvals[msg.sender][_id] == false);
        require(transferRequests[_id].hasBeenSent == false);

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

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

        if(transferRequests[_id].approvals >= limit){
            transferRequests[_id].hasBeenSent = true;
            transferRequests[_id].reciever.transfer(transferRequests[_id].amount);
            emit TransferApproved(_id);
        }
    }

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

Thanks, I will keep the better approach in mind, however it might be interesting to see how that could have been done. if you can’t find it no worries

2 Likes

I’m getting this error when my approval count is getting reached. I’m adding owners dynamically and updating limit depending upon the number of owners.

pragma solidity 0.7.5;

contract MultiSig{

    address[] owners;
    uint public limit;
    address payable contractBalance;

    struct Transfer{
        uint id;
        uint approvalCount;
        address[] approvers;
        uint amount;
        address payable recipient;
    }
    
    modifier ownerRestricted(){
        bool isOwner = false;
        for (uint i = 0; i < owners.length; i++ ){
            if(msg.sender == owners[i]){
                isOwner = true;
            }
        }
        require(isOwner == true );
        _;
    }

    Transfer[] transfers;

    function depositEther( ) public payable{
        contractBalance.transfer(msg.value);
    }

    function addOwners(address _owner ) public {
        bool alreayAnOwner = true;
        for(uint i = 0; i < owners.length ; i++ ){
            if( owners[i] == _owner ){
                alreayAnOwner = false;
            }
        }
        require(alreayAnOwner == true, "Already an owner");
        owners.push(_owner);
        changeLimit();
    }


    function changeLimit() private{
        if( owners.length %2 == 0 ){
            limit =  ( owners.length / 2 );
        }
        else{
            limit =  ( owners.length / 2 ) + 1;
        }
    }

    function createTransfer(uint _amount, address payable _receiver) public ownerRestricted{
        address[] memory empty;
        transfers.push( Transfer (transfers.length,0, empty ,_amount, _receiver));
    }

    function approveTransfer( uint _id ) public ownerRestricted{
        bool alreadyNotApproved = true;
        for( uint i = 0; i < transfers[_id].approvers.length; i++ ){
            if(transfers[_id].approvers[i] == msg.sender){
                alreadyNotApproved = false;
            }
        }
        if( transfers[_id].approvers.length == 0 ){
            alreadyNotApproved =  true;
        }
        require(alreadyNotApproved == true, " Here");
        transfers[_id].approvers.push(msg.sender);
        transfers[_id].approvalCount++;

        if( transfers[_id].approvalCount == limit){
            transfers[_id].recipient.transfer(transfers[_id].amount );
        }
    }

    function getTransfer(uint _id) public view returns( uint, uint, address[] memory, uint, address ){
        return (transfers[_id].id, transfers[_id].approvalCount,transfers[_id].approvers, transfers[_id].amount, transfers[_id].recipient);
    }


    function getBalanceOfContract() public view ownerRestricted returns (uint){
        return contractBalance.balance;
    }

}
1 Like