diff --git a/DynamicMultiSig_Wallet/.gitignore b/DynamicMultiSig_Wallet/.gitignore new file mode 100644 index 00000000..00dad773 --- /dev/null +++ b/DynamicMultiSig_Wallet/.gitignore @@ -0,0 +1,11 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + diff --git a/DynamicMultiSig_Wallet/.prettierrc.json b/DynamicMultiSig_Wallet/.prettierrc.json new file mode 100644 index 00000000..3e2ebf75 --- /dev/null +++ b/DynamicMultiSig_Wallet/.prettierrc.json @@ -0,0 +1,38 @@ +{ + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": false + } + }, + { + "files": "*.yml", + "options": {} + }, + { + "files": "*.yaml", + "options": {} + }, + { + "files": "*.toml", + "options": {} + }, + { + "files": "*.json", + "options": {} + }, + { + "files": "*.js", + "options": {} + }, + { + "files": "*.ts", + "options": {} + } + ] +} diff --git a/DynamicMultiSig_Wallet/README.md b/DynamicMultiSig_Wallet/README.md new file mode 100644 index 00000000..c2b2338f --- /dev/null +++ b/DynamicMultiSig_Wallet/README.md @@ -0,0 +1,27 @@ +# Dynamic MultiSig Wallet + +Medium Article: [Building and Deploying a Dynamic MultiSig Wallet With BuildBear Testnet.](https://medium.com/@molubode81/building-and-deploying-a-dynamic-multisig-wallet-with-buildbear-testnet-e27ceea7a055) + +### Features + +- Add new owner - A Proposal +- Confirm Proposal +- Execute Proposal +- Get All Proposals +- Get a Proposal +- Dynamically increment numConfirmation based on total owners + +### To start using this repository, simply clone it or download the zip file. Once downloaded, run `npm install` to install all the necessary packages. + +### To get your own Testnet head [builbear](https://buildbear.io/) + +- Create a Testnet on BuildBear +- Copy the RPC and explorer link from the Dashboard and add to the hardhat.config.js file or in .env + +### To deploy the dynamicmultisig contract, run `npx hardhat run scripts/deploy.js --network buildbear`. + +[Github Repository](https://github.com/SundayOlubode/Solidity-Tutorials/tree/SundayOlubode/DynamicMultiSig_Wallet) + +[Smart Contract Explorer](https://explorer.buildbear.io/wicked-rugor-nass-8d7a7a93/address/0x547D0D9D14252b97F759ab951d004EFA8Dbb4298) + +[Building and Deploying a Dynamic MultiSig Wallet With BuildBear Testnet.](https://medium.com/@molubode81/building-and-deploying-a-dynamic-multisig-wallet-with-buildbear-testnet-e27ceea7a055) diff --git a/DynamicMultiSig_Wallet/contracts/dynamicmultisig.sol b/DynamicMultiSig_Wallet/contracts/dynamicmultisig.sol new file mode 100644 index 00000000..d1cd1419 --- /dev/null +++ b/DynamicMultiSig_Wallet/contracts/dynamicmultisig.sol @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract dynamicmultisig{ + event Deposit(address indexed sender, uint amount, uint balance); + + event SubmitTransaction( + address indexed owner, + uint indexed txIndex, + address indexed to, + uint value, + bytes data + ); + event SubmitProposal(address indexed owner, uint indexed ppsIndex); + + event ConfirmTransaction(address indexed owner, uint indexed txIndex); + event ConfirmProposal(address indexed owner, uint indexed ppsIndex); + + event RevokeConfirmation(address indexed owner, uint indexed txIndex); + event RevokeProposal(address indexed owner, uint indexed ppsIndex); + + event ExecuteTransaction(address indexed owner, uint indexed txIndex); + event ExecuteProposal(address indexed owner, uint indexed ppsIndex); + + address[] public owners; + mapping(address => bool) public isOwner; + uint public numConfirmationsRequired; + + struct Transaction { + address to; + uint value; + bytes data; + bool executed; + uint numConfirmations; + } + + struct Proposal { + address newOwner; + bool executed; + uint numConfirmations; + } + + // mapping: map txIndex to owner address => tx confirmation + mapping(uint => mapping(address => bool)) isConfirmed; + + // mapping: map ppsIndex to owner address => pps confirmation + mapping(uint => mapping(address => bool)) ppsIsConfirmed; + + Transaction[] public transactions; + Proposal[] public proposals; + + modifier onlyOwner() { + require(isOwner[msg.sender], "not an owner"); + _; + } + + modifier txExists(uint _txIndex){ + require(_txIndex < transactions.length, "transaction does not exist"); + _; + } + + modifier ppsExists(uint _ppsIndex){ + require(_ppsIndex < proposals.length, "proposal does not exist"); + _; + } + + modifier notExecuted(uint _txIndex){ + require(!transactions[_txIndex].executed, "transaction already executed"); + _; + } + + modifier ppsNotExecuted(uint _ppsIndex){ + require(!proposals[_ppsIndex].executed, "proposal already executed"); + _; + } + + modifier notConfirmed(uint _txIndex){ + require(!isConfirmed[_txIndex][msg.sender], "transaction already confirmed"); + _; + } + + modifier ppsNotConfirmed(uint _ppsIndex){ + require(!ppsIsConfirmed[_ppsIndex][msg.sender], "proposal already confirmed"); + _; + } + + constructor(address[] memory _owners, uint _numConfirmationsRequired){ + require(_owners.length > 0, "owners required!"); + require( + _numConfirmationsRequired > 0 && + _numConfirmationsRequired < _owners.length, + "invalid number of required confirmations" + ); + + for(uint i = 0; i < _owners.length; i++){ + address owner = _owners[i]; + + require(owner != address(0), "invalid owner"); + require(!isOwner[owner], "owners must be unique!"); + + isOwner[owner] = true; + owners.push(owner); + } + + numConfirmationsRequired = _numConfirmationsRequired; + } + + // allow contract receive Ether + receive() external payable { + emit Deposit(msg.sender, msg.value, address(this).balance); + } + + /** Submit Transaction + */ + function submitTransaction( + address _to, + uint _value, + bytes memory _data + ) public onlyOwner { + + uint txIndex = transactions.length; + + transactions.push( + Transaction({ + to: _to, + value: _value, + data: _data, + executed: false, + numConfirmations: 0 + })); + + emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data); + } + + // Propose new owner + function addNewOwner(address _newOwner) + public + onlyOwner + { + // require _newOwner not existing + require(!isOwner[_newOwner], "Can't add existing owner"); + + uint ppsIndex = proposals.length; + + proposals.push( + Proposal({ + newOwner: _newOwner, + executed: false, + numConfirmations: 0 + })); + + emit SubmitProposal(_newOwner, ppsIndex); + } + + function comfirmTransaction(uint _txIndex) + public + onlyOwner + txExists(_txIndex) + notExecuted(_txIndex) + notConfirmed(_txIndex) + { + Transaction storage transaction = transactions[_txIndex]; + transaction.numConfirmations += 1; + isConfirmed[_txIndex][msg.sender] = true; + + emit ConfirmTransaction(msg.sender, _txIndex); + } + + function comfirmProposal(uint _ppsIndex) + public + onlyOwner + ppsExists(_ppsIndex) + ppsNotExecuted(_ppsIndex) + ppsNotConfirmed(_ppsIndex) + { + Proposal storage proposal = proposals[_ppsIndex]; + proposal.numConfirmations += 1; + ppsIsConfirmed[_ppsIndex][msg.sender] = true; + + emit ConfirmProposal(msg.sender, _ppsIndex); + + } + + function executeTransaction(uint _txIndex) + public + onlyOwner + txExists(_txIndex) + notExecuted(_txIndex) + { + Transaction storage transaction = transactions[_txIndex]; + + require( + transaction.numConfirmations >= numConfirmationsRequired, + "cannot execute transaction" + ); + + transaction.executed = true; + + (bool success, ) = transaction.to.call{value: transaction.value}( + transaction.data + ); + require(success, "transaction failed"); + + emit ExecuteTransaction(msg.sender, _txIndex); + } + + function executeProposal(uint _ppsIndex) + public + onlyOwner + ppsExists(_ppsIndex) + ppsNotExecuted(_ppsIndex) + { + Proposal storage proposal = proposals[_ppsIndex]; + + require( + proposal.numConfirmations >= numConfirmationsRequired, + "not enough confirmations!" + ); + + address _newOwner = proposal.newOwner; + + owners.push(_newOwner); + isOwner[_newOwner] = true; + proposal.executed = true; + + // Increment Number of Confirmations when a new owner is added + numConfirmationsRequired = owners.length / 2; + numConfirmationsRequired++; + + emit ExecuteProposal(msg.sender, _ppsIndex); + } + + function revokeConfirmation(uint _txIndex) + public + onlyOwner + txExists(_txIndex) + notExecuted(_txIndex) + { + Transaction storage transaction = transactions[_txIndex]; + + require(isConfirmed[_txIndex][msg.sender], "transaction not already confirmed"); + + transaction.numConfirmations -= 1; + isConfirmed[_txIndex][msg.sender] = false; + + emit RevokeConfirmation(msg.sender, _txIndex); + } + + function revokeProposal(uint _ppsIndex) + public + onlyOwner + ppsExists(_ppsIndex) + ppsNotExecuted(_ppsIndex) + { + Proposal storage proposal = proposals[_ppsIndex]; + + require(ppsIsConfirmed[_ppsIndex][msg.sender], "proposal not already confirmed"); + + proposal.numConfirmations -= 1; + ppsIsConfirmed[_ppsIndex][msg.sender] = false; + + emit RevokeConfirmation(msg.sender, _ppsIndex); + } + + function getOwners() public view returns (address[] memory) { + return owners; + } + + function getTransactionCount() public view returns (uint) { + return transactions.length; + } + + function getProposalCount() public view returns (uint) { + return proposals.length; + } + + function getAllTransactions() public view returns (Transaction[] memory) { + return transactions; + } + + function getAllProposals() public view returns (Proposal[] memory) { + return proposals; + } + + /** Get Proposal + */ + function getProposal(uint _ppsIndex) + public + view + returns ( + address newOwner, + bool executed, + uint numConfirmations + ) + { + Proposal storage proposal = proposals[_ppsIndex]; + + return ( + proposal.newOwner, + proposal.executed, + proposal.numConfirmations + ); + } + + /** Get Transaction + */ + function getTransaction(uint _txIndex) + public + view + returns ( + address to, + uint value, + bytes memory data, + bool executed, + uint numConfirmations + ) + { + Transaction storage transaction = transactions[_txIndex]; + + return ( + transaction.to, + transaction.value, + transaction.data, + transaction.executed, + transaction.numConfirmations + ); + } +} \ No newline at end of file diff --git a/DynamicMultiSig_Wallet/hardhat.config.js b/DynamicMultiSig_Wallet/hardhat.config.js new file mode 100644 index 00000000..bc016c38 --- /dev/null +++ b/DynamicMultiSig_Wallet/hardhat.config.js @@ -0,0 +1,12 @@ +require("@nomicfoundation/hardhat-toolbox"); +require("dotenv").config(); + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: "0.8.13", + networks: { + buildbear: { + url: process.env.BUILDBEAR_MAINNET_KEY, + }, + }, +}; diff --git a/DynamicMultiSig_Wallet/package.json b/DynamicMultiSig_Wallet/package.json new file mode 100644 index 00000000..9318deed --- /dev/null +++ b/DynamicMultiSig_Wallet/package.json @@ -0,0 +1,19 @@ +{ + "name": "dynamicmultisig_wallet", + "version": "1.0.0", + "description": "A dynamic multisig wallet", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Sunday Olubode", + "license": "ISC", + "dependencies": { + "@openzeppelin/contracts": "^4.9.3", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^1.0.2", + "hardhat": "^2.17.2" + } +} diff --git a/DynamicMultiSig_Wallet/scripts/deploy.js b/DynamicMultiSig_Wallet/scripts/deploy.js new file mode 100644 index 00000000..b6e31467 --- /dev/null +++ b/DynamicMultiSig_Wallet/scripts/deploy.js @@ -0,0 +1,34 @@ +// We require the Hardhat Runtime Environment explicitly here. This is optional +// but useful for running the script in a standalone fashion through `node