So this is the multi-sig wallet contract base.
This is a simplification of my actual project which is seperates the transaction and ownership elements into two contracts.
I am hoping to use some functionality from Gnosis, add owner, remover owner etc, I have achieved this, but currently only in a way that has a glaring security flaw, whereby a user can add and remove an owner without the need for a vote, so hypothetically, they could remove the original owners and add multiple of their own wallets to gain majority.
As it stands this contract example is transaction only, and owners must be set upon deployment with no possibility to modify ownership. Present state;
- Constructor sets owners in an array, and confirmation requirements.
- Addresses can be checked to see if they are owners.
- Any wallet or contract can deposit and send to the multi-sig.
- Mulit-sig can call other contracts
- Transaction struct can be queried through its array index to check confirmation, execution, to, value and data
- Confirmation can be revoked by any owner at any point before execution.
Can post without pseudo-code if it makes it less verbose.
/*
How to initialize owners and required confirmations, here 3 owners, 2/3 confirmations needed
["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"],
2
*/
//How to deposit (data must be even), 0xe4aE4dde9aA65e1C4C2A4BfbC040bB3675Abb6F6, 2000000000000000000, 0x00
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.6;
contract MultiSig {
//EVENTS
//Emit when deposit is made directly or when funds are sent to wallet.
event Deposit(address indexed sender, uint amount, uint balance);
//Emit when a tx proposal is registered
event Submit(
address indexed owner,
uint indexed txIndex,
address indexed to,
uint value,
bytes data
);
//Emit upon each confirmation
event Confirm(address indexed owner, uint indexed txIndex);
//Update total confirmations upon revokation
event Revoke(address indexed owner, uint indexed txIndex);
//Emit when threshold is met and tx is sent
event Execute(address indexed owner, uint indexed txIndex);
//CONSTANTS, STATE VARIABLES
uint constant public OWNER_LIMIT = 5;
//Number of confirmations to execute tx
uint public numConfirmationsRequired;
//Owners stored in array of addresses
address[] public owners;
//MAPPINGS
//To make sure there are no duplicate owners use a mapping to tell if an address is owner or not,
//Used in constructor late during owner initialization to avoid duplicating owners.
mapping(address => bool) public isOwner;
//Double mapping, uses txIndex and owner as inputs.
//Maps txIndex to Owner address, if they confirmed = true, if they did not = false
mapping(uint => mapping(address => bool)) public isConfirmed;
//STRUCTS
//Created by calling the submitTransaction function, stored in Transaction array below.
struct Transaction {
address to;
uint value;
//If calling another contract, tx data sent to that contract will be stored
bytes data;
//If tx is executed, stored as boolean
bool executed;
uint numConfirmations;
}
Transaction[] public transactions;
//MODIFIERS
modifier onlyOwner() {
//Uses prior mapping to check if function caller is one of the contract owner addresses.
require (isOwner[msg.sender], "You are not an owner.");
_;
}
//Takes in the txIndex as an input
modifier txExists(uint _txIndex) {
//If the input index is less than the array length, it exists in the array
require(_txIndex < transactions.length, "TX does not exist");
_;
}
modifier notExecuted(uint _txIndex) {
//Use bracket and dot notation to check value of executed in struct of given txIndex is false (!)
require(!transactions[_txIndex].executed, "Transaction already executed.");
_;
}
modifier notConfirmed(uint _txIndex) {
//Access double mapping, check if function caller has confirmed given Tx
require(!isConfirmed[_txIndex][msg.sender], "TX already confirmed");
_;
}
//CONSTRUCTOR to initialize array of owners and confimration requirement upon deployment of contract
constructor(address[] memory _owners, uint _numConfirmationsRequired) {
//Ensure that the array of owners is not empty
require(_owners.length > 0, "Owners required");
require(_numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length,
"Not enough confirmations"
);
//owners add as inputs are now added to the state variables.
for (uint i = 0; i < _owners.length; i++) {
//owner at index i stored in variable
address owner = _owners[i];
require(owner != address(0), "Not an owner");
//checking if address is already an owner using mapping
require(!isOwner[owner], "Owner already registered");
//If requirements are met, validate owner by setting it to true with mapping
isOwner[owner] = true;
//Add new owner to the state variable array of Owners
owners.push(owner);
}
//set the number of required confirmations with function input
numConfirmationsRequired = _numConfirmationsRequired;
}
//FUNCTIONS
//FALLBACK
receive() payable external {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
function deposit() payable external {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
//One of the owners will propose transaction
function submitTransaction (address _to, uint _value, bytes memory _data) public onlyOwner{
//the tx id will be it's index in the transactions array, 1=0, 2=1...
uint txIndex = transactions.length;
//Initialize Transaction struct and add to array of transactions
transactions.push(Transaction({
//to value and data set as inputs for the function call
to: _to,
value: _value,
data: _data,
//executed set to false as it is a new submission
executed: false,
//confirmations set to 0 as is cannot be confirmed before submission
numConfirmations: 0
}));
//Event emitted with function caller, current txIndex and inputs of function
emit Submit(msg.sender, txIndex, _to, _value, _data);
}
//Other owners can confirm transaction
function confirmTransaction(uint _txIndex)public onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {
//To update the Transaction struct, get tx by its txIndex, store as 'transaction'
Transaction storage transaction = transactions[_txIndex];
//set isConfirmed for the function caller on this particular tx to true.
//msg.sender has now approved this tx
isConfirmed[_txIndex][msg.sender] = true;
//Incredment numConfirmations
transaction.numConfirmations += 1;
emit Confirm(msg.sender, _txIndex);
}
//Allow owner to revoke permission for tx before it is notExecuted
//Functions as reverse of confirmation
function revokeConfirmation(uint _txIndex)public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
Transaction storage transaction = transactions[_txIndex];
require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
isConfirmed[_txIndex][msg.sender] = false;
transaction.numConfirmations -= 1;
emit Revoke(msg.sender, _txIndex);
}
function executeTransaction(uint _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex){
Transaction storage transaction = transactions[_txIndex];
require(transaction.numConfirmations >= numConfirmationsRequired, "Not enough Confirmations, cannot execute");
//Access executed setting of Tx ttruct for this tx and set to true
transaction.executed =true;
//Execute tx by using the call method
(bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
require(success, "tx failed");
emit Execute(msg.sender, _txIndex);
}
}