Project - Multisig Wallet

In my example contract the getter can also query address arrays within struct instances stored in a mapping, and return individual addresses by index number …

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;

contract StructArrayExample {

    struct Group {
        address[] addresses;
    }

    mapping(address => Group) groups;

    function addGroup() public {
        groups[msg.sender] = Group(new address[](0));
    }
    
    function addAddress(address newAddress) public {
        groups[msg.sender].addresses.push(newAddress);   
    }
    
    function getAddress(uint index) view public returns(address) {
        return groups[msg.sender].addresses[index];
    }
}
2 Likes

yeah it must have definitely been soe typo on my part. im gonna go back to what i wrote earier and see. tjats good to know though

1 Like

I don’t understand the line where you used the transfer call, is that a keyword or a function. Because it wasn’t declared in the contract.

1 Like

where are you reffering to ?

Yes, in the multisig wallet

Hwy @Talk2coded. Its just another way to transfer funds. payable(msg.sender).transfer(amount) is equivalent to msg.sender.call.value(amount)("")

I don’t understand.
What’s payable ?
What’s address.call.value. ?

1 Like

Ok. I fiest reccomend you lookinh up these 9n google and reasing the various foruma. Consensus has a good post on the whole thing ill link it below. Basically in the newer versions of solidity if we want to send ether to an address we nees to specidy the address as being paybale which is just a way of saying “ok this address is able to receive ether” thats why we have to say payable(msg.sender).transfer(amount) amd address.value.call another way to transfer funds. Its ba pretty muxh the same only it differs because transfer uses a fixed amount of gas 2300 because this protects against reentrancy attacks because 8n some cases
although ive never actually ran into any myself 2300 gas ia not enough gas to sometimes to call the callback functions which sends thw ether. Howecer .call doesnt have this gas limit so we dont have to worry about that but we do have to add extra security to prevent against rrentrancy attacks because of the lack of the gas cap. So if you using .call its reccomend to use

function withdraw() external { 
    uint256 amount = balanceOf[msg.sender]; 
    balanceOf[msg.sender] = 0; 
   (bool success, ) = msg.sender.call.value(amount)(""); 
   require(success, "Transfer failed."); 
}

You should read into this these are important concepts

boy-o! worlds beyond what i expected! lol
i’ll have to dig in later on, but yeah i totally appreciate how this conversation is here so i can come back to it to understand two different points of view converging

2 Likes

Ready!

pragma solidity 0.7.5;
pragma abicoder  v2;

contract Wallet {
    
    modifier onlyOwners {
        if (msg.sender == originalOwner) {
            _;
        }
        else {
            for (uint i =0 ; i < owners.length ; i++) {
                if (msg.sender == owners[i]) {
                    _;
                    break;
                }
            }
        }
    }
    
    modifier onlyOriginalOwner {
        require(msg.sender == originalOwner);
        _; 
    }
    
    address originalOwner;
    address[] private owners; //other guys 
    uint limit; 
    
    mapping (address => uint) deposits; // just to know how much anyone added
    
    struct TranferRequest {
        address payable to;
        uint ammount;
        uint approves; 
    }
    
    TranferRequest[] allRequests; 
    
    constructor() {
            originalOwner = msg.sender;
    }
    
    function addOwners(address[] memory _addresses) public onlyOriginalOwner{
        owners = _addresses;
    }
    
    function setLimit(uint _limit) public onlyOriginalOwner {
        limit = _limit;
    }
    
    function getRequests(uint _index) public view returns (address to, uint ammount, uint approves) {
        return (allRequests[_index].to, allRequests[_index].ammount, allRequests[_index].approves);
    }
    
    function getOwners() public view returns (address[] memory) {
        return owners;
    }
    
    function getLimit() public view returns(uint){
        return limit;
    }
    
    function deposit() payable public returns(uint) {
        deposits[msg.sender] += msg.value;
        return deposits[msg.sender];
    }
    
    function addRequest(address payable _to, uint _ammount) public onlyOwners {
        allRequests.push( TranferRequest(_to, _ammount, 0 ));
    }
    
    function approveRequest(uint _index) public onlyOwners {
        require(allRequests.length <= _index + 1 );
        allRequests[_index].approves += 1;
        if (allRequests[_index].approves == limit) {
            allRequests[_index].to.transfer(allRequests[_index].ammount);
        }
    }
    
    
}

Hello,

I wanted to solve this on my own, but I got stuck for a bit of time and I couldn’t really figure out how I could solve it, so decided to look at the template and solution video. I still managed to solve a few things on my own while looking at the template and the solution video and I also added the functionality that Filip talked about at the end of the video.

I plan to do the Multisig Wallet again after a few weeks, but this time on my own without looking at the solution, to challenge myself :muscle:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;
pragma abicoder v2;

contract MultisigWallet {
    
    address[] public owners;
    uint256 public approveLimit;
    
    struct Transfer {
        uint256 amount;
        address payable receiver;
        uint256 approvals;
        bool hasBeenSent;
        uint256 id;
    }
    
    event transferRequestCreated(uint256 _id, uint256 _amount, address _initiator, address _receiver);
    event approvalReceived(uint256 _id, uint256 _approvals, address _approver);
    event transferApproved(uint256 _id);
    
    Transfer[] transferRequests;
    
    mapping(address => mapping(uint256 => bool)) approvals;
    
    modifier onlyOwners() {
        bool owner = false;
        for(uint256 i = 0; i < owners.length; i++) {
            if(owners[i] == msg.sender) {
                owner = true;
            }
        }
        require(owner == true, "You are not the owner");
        _;
    }
    
    constructor(address[] memory _owners, uint256 _approveLimit) {
        owners = _owners;
        approveLimit = _approveLimit;
    }
    
    function deposit() external payable {}
    
    function createTransfer(uint256 _amount, address payable _receiver) external onlyOwners {
        require(address(this).balance >= _amount, "Not enough balance to transfer");
        emit transferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);

        Transfer memory newTransfer = Transfer(_amount, _receiver, 0, false, transferRequests.length);
        transferRequests.push(newTransfer);
    }
    
    function approve(uint256 _id) external onlyOwners {
        require(approvals[msg.sender][_id] == false, "You have already approved the transfer");
        require(transferRequests[_id].hasBeenSent == false, "Transfer has already been sent");
        
        approvals[msg.sender][_id] = true;
        transferRequests[_id].approvals++;
        
        emit approvalReceived(_id, transferRequests[_id].approvals, msg.sender);
        
        if(transferRequests[_id].approvals >= approveLimit) {
            transferRequests[_id].hasBeenSent = true;
            payable(transferRequests[_id].receiver).transfer(transferRequests[_id].amount);
            
            emit transferApproved(_id);
        }
    }
    
    function getTransferRequests() external view returns (Transfer[] memory TransferRequests) {
        return transferRequests;
    }
    
    function getBalance() external view returns (uint256 Balance) {
        return address(this).balance;
    }
}
1 Like

ok i had some time to pore over your guys’ code and conversation (thank you both)
i had one more question (and a comment below, spearately), but i think i get it for the most part: mappings are generally better than arrays (of struct or other) primarily because of having to loop through arrays due to the tedium of coding it and wasting gas.

what is the significance of the 0? is it that there can be many address arrays within this struct and we’ve chosen to instantiate the first (i.e. 0th) of them?
but…

so i’m confused about the “0” which, according to jon’s point, would limit the array to a length of 1 (not much of an array!); i must be missing something…

                          array          []            (0)

i.e. the above is in the form of “array name [array size (dynamic)] (array length)” length=0, right?

I totally hear you on the mapping point; i definitely got it once filip started talking about it, it’s just that i didn’t want to waste all my previous work having already started with this whole elaborate bunch of code that took me hours to figure out before checking hints! hahah
yeah, this is my first exposure to mappings, and i dig it.

1 Like

AHA! some pre-req pointers! i’ve been looking all around for those! i found an old set on https://www.castlecrypto.gg/ivan-on-tech-academy-review/
but it doesn’t seem to refer to the same-named courses any more.

nevertheless, i’m trying to understand the gamestop nft contract in order to figure out exactly what their big innovation is, so dapp programming will definitely be soon up on the agenda! should i take it before or after solidity201? is that were you learn react/vanilla js? the link avove seems to suggest 201 before smart contract or game programming
…also interested in polkadot primarily for understanding (not necessarily coding as a career), so that’ll be up after programming dapps

the forum is asking me to include other voices here, so @jon_m; have you any pointers for the order of courses i should follow from here (after i finish TA course, or if i should pause that even)?

1 Like

hey so when i saynew address[](0) it means unitalise an emty array becauuse its going to have nothing in it at the start because noone has approved the transaction yet. and also yes i would do sol 201 first before those two definitely becsase you still avent learned how to use tryffle and openzeppelin and some more complex festures within solidity such as testing.

2 Likes

Generally, yes, if you just want to look up individual values. But obviously when you need to iterate over more than one value (e.g. searching to see if a certain value is present, or to select and collate a separate array of values based on specific criteria) you will almost certainly need to use an array, because you won’t be able to iterate over a mapping.

No… just to add to what @mcgrane5 has already explained…

So, it can be used when you are creating a new struct instance based on a struct which contains one or more dynamically-sized arrays (as properties) e.g.

struct Group {
    bool exists;
    uint groupID;
    string groupName;
    address[] addresses;
}

mapping(address => Group) groups;

function addGroup(uint id, string memory name) public {
    groups[msg.sender] = Group(true, id, name, new address[](0));
    groups[msg.sender].addresses.push(msg.sender); 
}
// OR
function addGroup(uint id, string memory name) public {
    Group memory newGroup;
    newGroup.exists = true;
    newGroup.groupID = id;
    newGroup.groupName = name;
    newGroup.addresses = new address[](0);
    groups[msg.sender] = newGroup;
    groups[msg.sender].addresses.push(msg.sender);
}
// OR
function addGroup(uint id, string memory name) public {
    Group storage newGroup = groups[msg.sender];
    newGroup.exists = true;
    newGroup.groupID = id;
    newGroup.groupName = name;
    newGroup.addresses = new address[](0);
    newGroup.addresses.push(msg.sender);
}

/* To add additional addresses to the array stored in a specific Group instance
   which has already been created and saved in the mapping */
function addAddress(address newAddress) public {
    require(groups[msg.sender].exists, "You haven't created a group yet");
    groups[msg.sender].addresses.push(newAddress);   
}

We can’t create the dynamically-sized array property of a new struct instance using just  []  or  [newAddress]

But as @mcgrane5 has shown us, we can do it by defining an array with…

  1. The new keyword
  2. The data type of the values it will contain e.g. address[]
  3. A length of zero (0) — effectively an empty array, but which can have values added to it using the push method, once the new struct instance has been saved to persistent storage

… and assigning this definition to the property name (of the new struct instance) which will store the array value.


In another post I will pick up with, and try to explain, the other use of this syntax …

new dataType[](uint length)

… which I introduced into the mix, and which is different to what you appear to have understood from your comments in the second half of your post…

Yes, you are … see this post for an explanation.

mmmmm … sort of … but not quite :wink: … see this post for an explanation.

1 Like

This is my attempt at solving this on my own. Some feedback would be much appreciated. Especially if there are any improvements, best practices and security issues.

Ownable.sol

// SPDX-License-Identifier: MIT

pragma solidity 0.8.9;

contract Ownable {
    mapping(address => bool) internal owners;
    
    event OwnershipAssigned(address indexed owner);
    
    modifier onlyOwners {
        require(owners[msg.sender], "You are not one of the wallet owners");
        _;
    }
    
    constructor(address[] memory _owners) {
        owners[msg.sender] = true;
        emit OwnershipAssigned(msg.sender);
        
        for(uint i = 0; i < _owners.length; i++) {
            owners[_owners[i]] = true;
            emit OwnershipAssigned(_owners[i]);
        }
    }
}

Here I though, I could make this a bit more clean and dynamic by passing in an array of owners and just pick the number of owners according to the required approvals + the contract deployer. But i wanted to keep it simple and i am lazy… :grinning:
EDIT: Made the the changes to clean up the constructors and make everything dynamic.

Wallet.sol

// SPDX-License-Identifier: MIT

pragma solidity 0.8.9;

import "./Ownable.sol";

contract Wallet is Ownable {
    
    event DepositCompleted(address from, uint amount);
    event TransferRequestCreated(uint txId, uint amount, address recipient, address requestedBy);
    event TransderRequestApproved(uint txId, address approver);
    event TransderRequestCompleted(uint txId, uint amount, address recipient);
    
    uint8 private requiredApprovals;
    uint private txCount;
    
    struct TransferRequest {
        uint txId;
        uint amount;
        address recipient;
        mapping(address => bool) signatures;
        address requestedBy;
        uint8 approvals;
    }
    
    mapping(uint => TransferRequest) public requestedTransfers;
    
    /**
     * @dev one of the owners would be msg.sender during contract deployment
     */
    constructor(address[] memory _owners)
        Ownable(_owners) {
        requiredApprovals = uint8(_owners.length);
    }
    
    /**
     * @dev Deposit function. Along with the fallback function anyone can send funds to the contract
     */
    function deposit() external payable {
        emit DepositCompleted(msg.sender, msg.value);
    }
    
    receive() external payable {
        emit DepositCompleted(msg.sender, msg.value);
    }
    
    /**
     * @dev Requesting the transfer for approval
     */
    function requestTransfer(address _recipient, uint _amount) public onlyOwners returns (uint) {
        require(_amount <= address(this).balance, "Insufficient funds in the contract");
        
        TransferRequest storage txRequest = requestedTransfers[txCount];
        txRequest.txId = txCount;
        txRequest.amount = _amount;
        txRequest.recipient = _recipient;
        txRequest.requestedBy = msg.sender;
        
        emit TransferRequestCreated(txCount, _amount, _recipient, msg.sender);
        return txCount++;
    }
    
    /**
     * @dev Handles the approval of the transfer request. Only the owner of the wallet can approve.
     */
    function approveTransfer(uint _txId) public onlyOwners {
        TransferRequest storage txRequest = requestedTransfers[_txId];
        require(txRequest.requestedBy != msg.sender, "You can't approve your own transfer request");
        require(!txRequest.signatures[msg.sender], "You already approved this transder request");
        
        txRequest.signatures[msg.sender] = true;
        txRequest.approvals++;
        
        emit TransderRequestApproved(txRequest.txId, msg.sender);
        
        if(txRequest.approvals == requiredApprovals) {
           completeTransfer(txRequest.txId);
        }
    }
    
    /**
     * @dev Internal function to complete the requested transfer. This can only be called during the final approval vote
     * or when retrying due to Insufficient funds in the contract
     */
    function completeTransfer(uint _txId) internal {
        TransferRequest storage txRequest = requestedTransfers[_txId];
        require(txRequest.approvals == requiredApprovals, "Transfer not approved by all required parties.");
        require(txRequest.amount <= address(this).balance, "Insufficient funds in the contract");
        
        payable(txRequest.recipient).transfer(txRequest.amount);
        emit TransderRequestCompleted(txRequest.txId, txRequest.amount, txRequest.recipient);
    }
}
1 Like

pragma solidity 0.7.5;
pragma abicoder v2;

import “./Ownable.sol”;

contract MultiSigWallet is Ownable {

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

event TransferRequestCreated(uint _id, uint _amount, address _initiator, address _receiver);

Transfer[] transferRequests;

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



function deposit() public payable { 
}


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


function createTransfer(uint _amount, address payable _receiver) public onlyOwners {
    require(balance[msg.sender] >= _amount);
    require(msg.sender != _receiver);
    
    emit TransferRequestCreated(transferRequests.length, _amount, msg.sender, _receiver);
    
    transferRequests.push( 
        Transfer(_amount, _receiver, 0, false, transferRequests.length) 
    );
} 


function approve(uint _id) public onlyOwners {
    require(approvals[msg.sender][_id] == false, "Dont vote twice");
    require(transferRequests[_id].hasBeenSent == false, "Not able to vote after sending request");
    
    approvals[msg.sender][_id] = true;
    transferRequests[_id].approvals++;
    
    if(transferRequests[_id].approvals >= limit) {
        transferRequests[_id].hasBeenSent = true;
        transferRequests[_id].receiver.transfer(transferRequests[_id].amount);
    }
}


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

}

pragma solidity 0.7.5;

import “./Wallet”;

contract Ownable is MultiSigWallet {

address[] public owners;
uint limit;

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) {
    owners = _owners;
    limit = _limit;
}

}

1 Like

I ended up making something a bit different (and hopefully smarter) than what was originally requested by the assignment as I wanted to achieve something a little more flexible, I hope this is not a problem!

Here is a summary of the features I have implemented:

  • The contract acts like a bank and allows any number of multisig accounts to be created. Each account has a unique ID (uint) and a custom alias (string).

  • The default minimum signatures is set to 2 via constructor, but this requirement can be changed on each account if ALL of its members agree to do so.

  • Any address can create an account (and automatically become a member of it).

  • Account members can candidate new members to be added (the function “signAccountOption” can be used to approve them).

  • As long as the account has only one member, every operation (deposit, withdraw, transfer, add member, change account settings) doesn’t require additional signatures and is executed right away.

  • After a second member has been added to the account, every action (except deposits) requires additional signatures.

  • New account members are not allowed to sign any pending transaction/operation requested before they were added to the account.

  • Owner can: pause/unpause the contract (view functions are not paused), transfer the contract ownership (the new owner must accept), set a new default for minimum signatures (which affects only new accounts).

  • Anyone can: create a new account, check the balance and members of any account, check the settings (such as minimum signatures required) of any account, check the signatories (addresses allowed to sign) of any transaction/operation of any account, check the list and state of pending transactions of any account.

  • Account members can: deposit, request transfers/withdraws, request account changes, sign all pending operations requested after they joined the account

The code is a bit long, so here is the link to the github repository I created to work on it:

github.com/vale-dinap/multisig_wallet

File1: type_definitions.sol

// Custom data types

pragma solidity ^0.8.9;
import "./utility_functions.sol"; // Utility functions used in the contract

library multisig_dataTypes{

    struct Signature{ // Data type to store pending signatures
        address[] signatories; // this will be filled with the addresses allowed to sign: after an allowed address signs, the "count" grows by 1 and the address is removed from the array (so that it won't be able to sign again)
        uint count;
    }

    struct Transaction{ // Data type to store pending transactions
        address payee;
        uint value;
        Signature signature;
    }

    enum OptionType {addMember, changeSignaturesRequired, changeLabel}

    struct AccountOption{ // Data type to store pending account changes
        OptionType option;
        address addressData; // address data container, the contents can vary depending on the specific account option
        uint uintData; // uint data container, the contents can vary depending on the specific account option
        string stringData; // string data container, the contents can vary depending on the specific account option
        Signature signature;
    }
    
    
    // NOTE: The following functions allow to output the data from the structs above as a human-readable formatted string. They are meant for debug purposes only as string manipulation uses a lot of resources.
    
    /*
    function decodeTransactionData(Transaction memory transaction) pure internal returns (string memory result){  // SLOW, using for debug purposes
        string memory signatories;
        for(uint i=0; i<transaction.signature.signatories.length; ++i){
            if(transaction.signature.signatories[i] != address(0)){
                signatories=string(abi.encodePacked(signatories, ", ", utilityLib.addressToString(transaction.signature.signatories[i])));
            }
        }
        result=string(abi.encodePacked("Send ", utilityLib.uintToString(transaction.value), " wei to address ", utilityLib.addressToString(transaction.payee), ". Signed ", utilityLib.uintToString(transaction.signature.count), " times. Can be signed by ", signatories));
    }
    
    function decodeAccountOptionData(AccountOption memory accOption) pure internal returns (string memory result){  // SLOW, using for debug purposes
        string memory optionDescr;
        if(uint(accOption.option) == 0){  // Add a member
            optionDescr = string(abi.encodePacked("Add member, address: ", utilityLib.addressToString(accOption.addressData) ));
        }
        else if(uint(accOption.option) == 1){  // Change number of signatures required for transactions
            optionDescr = string(abi.encodePacked("Change amount of required signatures to: ", utilityLib.uintToString(accOption.uintData) ));
        }
        else if(uint(accOption.option) == 2){  // Change the group label
            optionDescr = string(abi.encodePacked("Change account label to: ", accOption.stringData ));
        }
            
        string memory signatories;
        for(uint i=0; i<accOption.signature.signatories.length; ++i){
            if(accOption.signature.signatories[i] != address(0)){
                signatories=string(abi.encodePacked(signatories, ", ", utilityLib.addressToString(accOption.signature.signatories[i])));
            }
        }
        result=string(abi.encodePacked(optionDescr, ". Signed ", utilityLib.uintToString(accOption.signature.count), " times. Can be signed by ", signatories));
    }
    */
}

File 2: utility_functions.sol

// Utility functions

pragma solidity ^0.8.9;

library utilityLib{

    // looks for the given address in the given array, if found returns the index of the first match, otherwise -1
    function find(address[] memory list, address member) internal pure returns(int){
        int index=-1;
        for(uint i=0; i<list.length; i++){
            if(member == list[i]){
                return int(i);
            }
        }
        return index;
    }

    // wrapper of "find", returns a bool (true if found); I could have avoided this but helps making the main contract's code cleaner and easier to read/amend
    function inList(address[] memory list, address member) internal pure returns(bool found){
        found = find(list, member)>=0;
    }
    
    // finds and removes a known element from a dynamic address array (by actually rebuilding it)
    function removeItem(address[] memory list, address member) internal pure returns(address[] memory newList){
        if(inList(list, member)){
            uint k;
            for(uint i=0; i<list.length; i++){
                if(member != list[i]){
                    newList[k]=list[i];
                    k++;
                }
            }
        }
        else{newList=list;}
    }

    // Minimum between two numbers
    function min(uint _valueA, uint _valueB) internal pure returns(uint result){
        result = (_valueA<_valueB ? _valueA : _valueB);
    }




    // Convert uint to string - source: StackOverflow website
    function uintToString(uint _i) internal pure returns (string memory str){
        if (_i == 0){return "0";}
        uint j = _i;
        uint length;
        while (j != 0){
            length++;
            j /= 10;
            
        }
        bytes memory bstr = new bytes(length);
        uint k = length;
        j = _i;
        while (j != 0){
            bstr[--k] = bytes1(uint8(48 + j % 10));
            j /= 10;
            
        }
        str = string(bstr);
    }
    
    // Convert address to string - source: StackOverflow website
    function addressToString(address addr) internal pure returns(string memory) {
        bytes32 value = bytes32(uint256(uint160(addr)));
        bytes memory alphabet = "0123456789abcdef";
        bytes memory str = new bytes(51);
        str[0] = "0";
        str[1] = "x";
        for (uint i = 0; i < 20; i++) {
            str[2+i*2] = alphabet[uint(uint8(value[i + 12] >> 4))];
            str[3+i*2] = alphabet[uint(uint8(value[i + 12] & 0x0f))];
            
        }
        return string(str);
}


}

File 3: ownable.sol

// Base contract allowing owner functions

pragma solidity ^0.8.9;

contract Ownable{
    address owner;
    address ownerProposed;

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

    // modifiers
    modifier onlyOwner {
        require(msg.sender == owner, "Only contract owner can do this");
        _;
    }

    function offerOwnership(address newOwner) public onlyOwner {  // Candidate new address for ownership
        ownerProposed = newOwner;
    }
    
    function acceptOwnership() public {  // Accept contract ownership
        require(ownerProposed == msg.sender, "You are not allowed to claim the ownership of the contract");
        owner = msg.sender;
        ownerProposed = address(0);
    }

}

File 4: pausable.sol

// Pausable contract (owner only)

pragma solidity ^0.8.9;

import "./ownable.sol";

contract Pausable is Ownable{
    bool paused;

    // Constructor
    constructor(){
        paused = false;
    }

    // Functions to pause and unpause the contract
    function pauseContract() public onlyOwner {
        paused=true;
    }

    function unpauseContract() public onlyOwner {
        paused=false;
    }

    // Modifier to run functions only if the contract is not paused
    modifier notPaused {
        require(paused==false, "This function has been temporarily paused by the admin");
        _;
    }

}

File 5: accountable.sol

pragma solidity ^0.8.9;
pragma abicoder v2;
import "./pausable.sol";  // Base contract making the contract pausable and adding admin controls (as Pausable is also Ownable)
import {multisig_dataTypes as MultisigType} from "./type_definitions.sol";  // Custom data types are defined in a separate library file
import "./utility_functions.sol"; // Utility functions used in the contract

/*
Terms used in variables:
-Account: account on this bank, can have any given number of wallets as its members. Accounts are identified by ID.
-Member: wallet belonging to a sepcific account. A wallet can be member of multiple accounts.
-Transaction: any operation involving transfer of funds (deposit, withdraw, transfer). If the account has more than one member, withdraw and transfer remain pending until enough signatures are provided.
-Pending transactions: each account has an array working as a buffer of transactions that have been requested by one of its members but haven been signed enough times.
-Transaction ID: the index of a transaction within the array of pending transactions.
-Signatories: wallet addresses allowed to sign a specific transaction; their list is embedded in each pending transaction by the time the transaction is created.
            This is important to prevent a new wallet added to the account from signing any transactions created earlier.
*/

contract Accountable is Pausable{  // Pausable is also Ownable by multi-level inheritance

    uint public numberOfAccounts;  //Initialize accounts counter
    uint defaultSignaturesRequired;  //Initialize global state variable for minimum number of signatures


    constructor(){
        defaultSignaturesRequired = 2;  //Define default minimum number of signatures required to run a Tx
    }


    // MAPPINGS

    mapping (uint => address[]) members; // store an array of members for each account
    mapping (uint => MultisigType.Transaction[]) pendingTransactions; // store an array of pending Tx for each account (hopefully it should be a good optimization in both terms of data structure and gas cost)
    mapping (uint => uint) balance; // store account balance
    mapping (uint => string) label; // store an optional account label for easy recognition
    mapping (uint => uint) signaturesRequired; // store per-account override on required amount of signatures
    mapping (uint => MultisigType.AccountOption[]) pendingSettings; // store an array of pending settings changes for each account


    // MODIFIERS

    modifier existingAccount(uint accountId){ // This ensures that functions are executed only on existing accounts
        require(accountId < numberOfAccounts, "This account ID does not exist");
        _;
    }

    modifier onlyMember(uint accountId){ // This ensures that sender can run functions only on accounts he/she is member of
        require(utilityLib.inList(members[accountId], msg.sender), "Your cannot do this on accounts you are not member of");
        _;
    }

    modifier existingAccountSettingProposal(uint accountId, uint pendingOptionId){ // This ensures that functions are executed only on existing account amendments proposals
        require(pendingOptionId < pendingSettings[accountId].length, "This account setting proposal ID does not exist");
        _;
    }


    // SIGNATURE FUNCTION
    
    function sign(MultisigType.Signature storage signature) internal notPaused {
        require(utilityLib.inList(signature.signatories, msg.sender), "You are not allowed to sign this (or you have already signed)");
        int index = utilityLib.find(signature.signatories, msg.sender);
        delete signature.signatories[uint(index)];
        signature.count+=1;
    }
    
    /* //Alternative sign function (generates errors in some cases, needs debug, will use the other for now)
    function sign(MultisigType.Signature storage signature) internal notPaused {
        require(utilityLib.inList(signature.signatories, msg.sender), "You are not allowed to sign this (or you have already signed)");
        signature.signatories=utilityLib.removeItem(signature.signatories, msg.sender);
        signature.count+=1;
    }*/


    // PRIVATE FUNCTIONS

    ///////////////////////////// The function below is overloaded /////////////////////////////
    function buildAccountOption(uint accountId, MultisigType.OptionType option, address data) private notPaused existingAccount(accountId) onlyMember(accountId) returns(uint) { // Function overload for "address" input
        uint curOptAmount = pendingSettings[accountId].length;
        MultisigType.AccountOption memory newOption;
        newOption.option = option;
        newOption.addressData = data;
        newOption.signature.signatories=members[accountId];
        newOption.signature.count=0;
        pendingSettings[accountId].push(newOption);
        uint newOptionId = pendingSettings[accountId].length-1;
        assert(curOptAmount == (pendingSettings[accountId].length-1));
        return newOptionId;
    }

    function buildAccountOption(uint accountId, MultisigType.OptionType option, uint data) private notPaused existingAccount(accountId) onlyMember(accountId) returns(uint) { // Function overload for "uint" input
        uint curOptAmount = pendingSettings[accountId].length;
        MultisigType.AccountOption memory newOption;
        newOption.option = option;
        newOption.uintData = data; // Specific data type for this overloaded function, the rest of the function body is unchanged
        newOption.signature.signatories=members[accountId];
        newOption.signature.count=0;
        pendingSettings[accountId].push(newOption);
        uint newOptionId = pendingSettings[accountId].length-1;
        assert(curOptAmount == (pendingSettings[accountId].length-1));
        return newOptionId;
    }

    function buildAccountOption(uint accountId, MultisigType.OptionType option, string memory data) private notPaused existingAccount(accountId) onlyMember(accountId) returns(uint) { // Function overload for "string" input
        uint curOptAmount = pendingSettings[accountId].length;
        MultisigType.AccountOption memory newOption;
        newOption.option = option;
        newOption.stringData = data; // Specific data type for this overloaded function, the rest of the function body is unchanged
        newOption.signature.signatories=members[accountId];
        newOption.signature.count=0;
        pendingSettings[accountId].push(newOption);
        uint newOptionId = pendingSettings[accountId].length-1;
        assert(curOptAmount == (pendingSettings[accountId].length-1));
        return newOptionId;
    }
    ///////////////////////////// End of function overload /////////////////////////////////////

    function changeAccountOption(uint accountId, uint settingProposalId) private notPaused existingAccount(accountId) onlyMember(accountId) existingAccountSettingProposal(accountId, settingProposalId) {

        // Check signatures needed to perform the option change
        uint requiredSignatures;
        uint optionType = uint(pendingSettings[accountId][settingProposalId].option);
        if(optionType == 0){requiredSignatures=signaturesRequired[accountId];} // Signatures needed to add a member
        else if(optionType == 1){requiredSignatures=members[accountId].length;} // Signatures needed to change number of signatures required for transactions - full consensus!
        else if(optionType == 2){requiredSignatures=signaturesRequired[accountId];} // Signatures needed to change the group label

        requiredSignatures = utilityLib.min(requiredSignatures, members[accountId].length); // Now override the number of signatures needed IF the account contains fewer members than required

        require(pendingSettings[accountId][settingProposalId].signature.count>=requiredSignatures, "Not enough signatures");

        // Perform change
        if(optionType == 0){  // Add a member
            address addressData = pendingSettings[accountId][settingProposalId].addressData;
            require(utilityLib.inList(members[accountId], addressData) == false, "The wallet you are attempting to add is already a member of the selected account");
            members[accountId].push(addressData);}
        else if(optionType == 1){  // Change number of signatures required for transactions
            uint uintData = pendingSettings[accountId][settingProposalId].uintData;
            signaturesRequired[accountId]=uintData;}
        else if(optionType == 2){  // Change the group label
            string memory stringData = pendingSettings[accountId][settingProposalId].stringData;
            label[accountId]=stringData;}
    }


    // INTERFACE FUNCTIONS

    function signAccountOption(uint _accountId, uint _settingProposalId) public notPaused existingAccount(_accountId) onlyMember(_accountId) existingAccountSettingProposal(_accountId, _settingProposalId) {
        sign(pendingSettings[_accountId][_settingProposalId].signature); // Do sign

        // Check signatures needed to also perform the option change
        uint requiredSignatures;
        uint optionType = uint(pendingSettings[_accountId][_settingProposalId].option);
        if(optionType == 0){requiredSignatures=signaturesRequired[_accountId];} // Signatures needed to add a member
        if(optionType == 1){requiredSignatures=members[_accountId].length;} // Signatures needed to change number of signatures required for transactions - needs full consensus
        if(optionType == 2){requiredSignatures=signaturesRequired[_accountId];} // Signatures needed to change the group label

        requiredSignatures = utilityLib.min(signaturesRequired[_accountId], members[_accountId].length); // Now override the number of signatures required IF the account contains fewer members

        // Finally perform the change (if enough signatures)
        if(pendingSettings[_accountId][_settingProposalId].signature.count >= requiredSignatures){
            changeAccountOption(_accountId, _settingProposalId);
        }
    }

    function createAccount(string memory _label) public notPaused { // Creates a new account, the wallet creating it becomes automatically its first member
        uint newAccountId = numberOfAccounts;
        members[newAccountId].push(msg.sender);
        balance[newAccountId] = 0;
        label[newAccountId] = _label;
        signaturesRequired[newAccountId] = defaultSignaturesRequired; // Initialize with global value; this can be later amended by the account owners (with full consensus) - I HAVEN'T WRITTEN YET THE FUNCTION TO DO THIS
        numberOfAccounts+=1;
    }

    function addMember(uint _accountId, address _newMemberAddress) public notPaused existingAccount(_accountId) onlyMember(_accountId) {
        require(utilityLib.inList(members[_accountId], _newMemberAddress) == false, "The wallet you are attempting to add is already a member of the selected account");
        uint optionId = buildAccountOption(_accountId, MultisigType.OptionType(0), _newMemberAddress);
        signAccountOption(_accountId, optionId);
    }

    function getMembers(uint _accountId) public view existingAccount(_accountId) returns(address[] memory){  // Return all members of the account id specified
        return members[_accountId];
    }

    function setAccountLabel(uint _accountId, string memory _newLabel) public notPaused existingAccount(_accountId) onlyMember(_accountId) {
        uint optionId = buildAccountOption(_accountId, MultisigType.OptionType(2), _newLabel);
        signAccountOption(_accountId, optionId);
    }

    function getAccountLabel(uint _accountId) public view existingAccount(_accountId) returns(string memory){  // Return the label of the given account ID
        return label[_accountId];
    }

    function setSignaturesRequired(uint _accountId, uint _newAmount) public notPaused existingAccount(_accountId) onlyMember(_accountId) {
        uint optionId = buildAccountOption(_accountId, MultisigType.OptionType(1), _newAmount);
        signAccountOption(_accountId, optionId);
    }
    
    function setDefaultSignaturesRequired(uint _newAmount) public onlyOwner {
        defaultSignaturesRequired = _newAmount;
    }
    
    function getSignaturesRequired(uint _accountId) view public existingAccount(_accountId) returns(uint){
        uint amount = signaturesRequired[_accountId];
        return amount;
    }


    // SIGNATURE LOOKUP

    function countOptionChangeSignatures(uint _accountId, uint _settingProposalId) public view existingAccount(_accountId) existingAccountSettingProposal(_accountId, _settingProposalId) returns(uint){
        uint count = pendingSettings[_accountId][_settingProposalId].signature.count;
        return count;
    }

    function getOptionChangeSignatories(uint _accountId, uint _settingProposalId) public view existingAccount(_accountId) existingAccountSettingProposal(_accountId, _settingProposalId) returns(address[] memory){
        address[] memory signatories = pendingSettings[_accountId][_settingProposalId].signature.signatories;
        return signatories;
    }
    
    
    // HUMAN-READABLE OPTION LOOKUP (outputs all info formatted as a string) - slow, using for debug purposes - to use them, also uncomment the last part of the file "type_definitions"
    /*
    function printSettingProposalData(uint _accountId, uint _settingProposalId) public view existingAccount(_accountId) existingAccountSettingProposal(_accountId, _settingProposalId) returns(string memory propData){
        propData = string(abi.encodePacked(
            "Account ID: ", utilityLib.uintToString(_accountId), 
            ", proposal ID: ", utilityLib.uintToString(_settingProposalId), ". ",
            MultisigType.decodeAccountOptionData(pendingSettings[_accountId][_settingProposalId])
                ));
    }
    */
}

File 6: multisig_bank.sol

pragma solidity ^0.8.9;
pragma abicoder v2;
import "./pausable.sol";  // Base contract making the contract pausable and adding admin controls (as Pausable is also Ownable)
import {multisig_dataTypes as MultisigType} from "./type_definitions.sol";  // Custom data types are defined in a separate library file
import "./utility_functions.sol"; // Utility functions used in the contract
import "./accountable.sol";  // Base contract adding account management functionalities, making the contract pausable and adding admin controls (Accountable is Pausable, Pausable is Ownable)

/*
Terms used in variables:
-Account: account on this bank, can have any given number of wallets as its members. Accounts are identified by ID.
-Member: wallet belonging to a sepcific account. A wallet can be member of multiple accounts.
-Transaction: any operation involving transfer of funds (deposit, withdraw, transfer). If the account has more than one member, withdraw and transfer remain pending until enough signatures are provided.
-Pending transactions: each account has an array working as a buffer of transactions that have been requested by one of its members but haven been signed enough times.
-Transaction ID: the index of a transaction within the array of pending transactions.
-Signatories: wallet addresses allowed to sign a specific transaction; their list is embedded in each pending transaction by the time the transaction is created.
            This is important to prevent a new wallet added to the account from signing any transactions created earlier.
*/

contract MultisigBank is Accountable{  // Accountable is also Pausable and Ownable by multi-level inheritance


    // MODIFIERS

    modifier existingTransaction(uint accountId, uint transactionId){ // This ensures that functions are executed only on existing transactions
        require(transactionId < pendingTransactions[accountId].length, "This transaction ID does not exist");
        _;
    }

    modifier hasEnoughFunds(uint accountId, uint value){ // This ensures that the account has enough founds to perform the action.
        require(value<=getBalance(accountId), "The account has not enough funds to execute this transaction");
        _;
    }


    // PRIVATE FUNCTIONS

    function buildTransaction(uint accountId, address payee, uint value) private notPaused existingAccount(accountId) onlyMember(accountId) hasEnoughFunds(accountId, value) returns(uint) {
        MultisigType.Transaction memory newTransaction;
        newTransaction.payee = payee;
        newTransaction.value = value;
        newTransaction.signature.signatories=members[accountId];
        newTransaction.signature.count=0;
        pendingTransactions[accountId].push(newTransaction);
        uint newTransactionId = pendingTransactions[accountId].length-1;
        return newTransactionId;
    }

    function runTransaction(uint accountId, uint transactionId) private notPaused hasEnoughFunds(accountId, pendingTransactions[accountId][transactionId].value) {
        uint requiredSignatures = utilityLib.min(signaturesRequired[accountId], members[accountId].length);
        require(pendingTransactions[accountId][transactionId].signature.count>=requiredSignatures, "Not enough signatures");

        uint origAccountBalance = balance[accountId]; //used in assert stataments
        uint balanceSum = pendingTransactions[accountId][transactionId].payee.balance + address(this).balance; //used in assert stataments
        
        balance[accountId] -= pendingTransactions[accountId][transactionId].value;
        payable(pendingTransactions[accountId][transactionId].payee).transfer(pendingTransactions[accountId][transactionId].value);
        
        assert(balance[accountId] == origAccountBalance - pendingTransactions[accountId][transactionId].value); //check new account balance
        assert(balanceSum == pendingTransactions[accountId][transactionId].payee.balance + address(this).balance); //zero sum check

        delete pendingTransactions[accountId][transactionId];

    }


    // TRANSACTIONS

    function getBalance(uint _accountId) view public existingAccount(_accountId) returns(uint){
        uint value = balance[_accountId];
        return value;
    }

    function listPendingTransactions(uint _accountId) view public existingAccount(_accountId) returns(MultisigType.Transaction[] memory){
        MultisigType.Transaction[] memory list = pendingTransactions[_accountId];
        return list;
    }

    function signTransaction(uint _accountId, uint _transactionId) public notPaused existingAccount(_accountId) onlyMember(_accountId) existingTransaction(_accountId, _transactionId) hasEnoughFunds(_accountId, pendingTransactions[_accountId][_transactionId].value){
        sign(pendingTransactions[_accountId][_transactionId].signature);
        uint requiredSignatures = utilityLib.min(signaturesRequired[_accountId], members[_accountId].length);
        if(pendingTransactions[_accountId][_transactionId].signature.count >= requiredSignatures){
            runTransaction(_accountId, _transactionId);
        }
    }

    function deposit(uint _accountId) public payable notPaused existingAccount(_accountId) onlyMember(_accountId) {
        
        uint origAccountBalance = balance[_accountId]; //used in assert stataments
        uint balanceSum = msg.sender.balance + address(this).balance; //used in assert stataments

        balance[_accountId]+=msg.value; //add deposit to account balance
        
        assert(balance[_accountId] == origAccountBalance + msg.value); //check new account balance
        assert(balanceSum == msg.sender.balance + address(this).balance); //zero sum check
    }

    function withdraw(uint _accountId, uint _value) public notPaused existingAccount(_accountId) onlyMember(_accountId) hasEnoughFunds(_accountId, _value){
        uint transactionId = buildTransaction(_accountId, msg.sender, _value);
        signTransaction(_accountId, transactionId);
    }

    function transfer(uint _accountId, address _payee, uint _value) public notPaused existingAccount(_accountId) onlyMember(_accountId) hasEnoughFunds(_accountId, _value){
        uint transactionId = buildTransaction(_accountId, _payee, _value);
        signTransaction(_accountId, transactionId);
    }


    // SIGNATURE LOOKUP

    function countTransactionSignatures(uint _accountId, uint _transactionId) public view existingAccount(_accountId) existingTransaction(_accountId, _transactionId) returns(uint){
        uint count = pendingTransactions[_accountId][_transactionId].signature.count;
        return count;
    }

    function getTransactionSignatories(uint _accountId, uint _transactionId) public view existingAccount(_accountId) existingTransaction(_accountId, _transactionId) returns(address[] memory){
        address[] memory signatories = pendingTransactions[_accountId][_transactionId].signature.signatories;
        return signatories;
    }
    
    // HUMAN-READABLE TRANSACTION LOOKUP (outputs all info formatted as a string) - slow, using for debug purposes - to use them, also uncomment the last part of the file "type_definitions"
    /*
    function printTransactionData(uint _accountId, uint _transactionId) public view existingAccount(_accountId) existingTransaction(_accountId, _transactionId) returns(string memory txData){
        txData = string(abi.encodePacked(
            "Account ID: ", utilityLib.uintToString(_accountId), 
            ", transaction ID: ", utilityLib.uintToString(_transactionId), ". ",
            MultisigType.decodeTransactionData(pendingTransactions[_accountId][_transactionId])
                ));
    }
    */
}

I have to say I had a very good time coding this! :smiley:

Any feedbacks/suggestions would be massively appreciated.

1 Like

hey @vale_dinap. very impressive man you seem to have really nailed the goverance here. I intent to have a good look at your code when i get the chance but from what im seeing youve nailed it.

1 Like