├── Multisig_wallet_info.md ├── README.md ├── contracts ├── Migrations.sol ├── MultiSignatureWallet.sol └── SimpleStorage.sol ├── gifs └── remix.gif ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js └── truffle.js /Multisig_wallet_info.md: -------------------------------------------------------------------------------- 1 | # Project 3: Create a Multi-signature Wallet 2 | 3 | **Background:** Blockchains are tricky things; the promise of immutability seems oh-so-nice until you accidentally lose your private key that controls most of your net worth. Then, immutability sucks. 4 | 5 | To protect themselves against this type of loss, users quickly realized they could use a multiple signature wallet. Depending on the implementation, this wallet allows for a quorum (say, 50%) of keys that control it to use the funds. 6 | 7 | These wallets are also used by companies/projects that control large amounts of crypto-currency, to reduce the risk that a team member will run away with huge sums of money. To protect our class from such an event, we will all be making multi-sig wallets of our own. 8 | 9 | If you want to read more about multi-sig wallets, and see what they were like in the Bitcoin era. 10 | 1. https://en.bitcoin.it/wiki/Multisignature 11 | 2. https://ethereum.stackexchange.com/questions/6/how-can-i-create-a-multisignature-address-on-ethereum 12 | 13 | **Objectives:** 14 | - To learn how to handle complex interactions between multiple users on one contract 15 | - Learn how to avoid loops and implement voting 16 | - To learn to assess possible attacks on contracts. 17 | 18 | **Directions:** 19 | Implement a multi-signature wallet. [See here for function stubs.](./multisig/contracts/MultiSignatureWallet.sol) 20 | 21 | Thanks to Nate Rush 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This walkthrough is based on [this project](./Multisig_wallet_info.md) by Nate Rush. 2 | 3 | 4 | The solution is based on [this MultiSignature Wallet](https://github.com/ConsenSys/MultiSigWallet) found in the ConsenSys github repository. 5 | 6 | 7 | # What is a Multisignature wallet? 8 | 9 | 10 | A multisignature wallet is an account that requires some m-of-n quorum of approved private keys to approve a transaction before it is executed. 11 | 12 | 13 | Ethereum implements multisignature wallets slightly differently than Bitcoin does. In Ethereum, multisignature wallets are implemented as a smart contract, that each of the approved external accounts sends a transaction to in order to "sign" a group transaction. 14 | 15 | 16 | Following this project spec designed by the UPenn Blockchain Club, you will now create your own multisignature wallet contract. 17 | 18 | 19 | **Note: It is not suggested that you use this multisignature wallet with any real funds, but rather use a far more deeply audited one such as the [Gnosis multisignature wallet.](https://wallet.gnosis.pm/)** 20 | 21 | 22 | **Objectives:** 23 | 1. To learn how to handle complex interactions between multiple users on one contract 24 | 2. Learn how to avoid loops and implement voting 25 | 3. [To learn to assess possible attacks on contracts.](https://github.com/ConsenSys/smart-contract-best-practices) 26 | 27 | Table of contents 28 | ================= 29 | 30 | * [Project Setup](#project-setup) 31 | * [Implementing the Contract](#implementing-the-contract) 32 | * [Constructor](#constructor) 33 | * [Submit Transaction](#submit-transaction) 34 | * [Add Transaction](#add-transaction) 35 | * [Confirm Transaction](#confirm-transaction) 36 | * [Execute Transaction](#execute-transaction) 37 | * [Additional Functions](#additional-functions) 38 | * [Interacting with the Contract](#interacting-with-the-contract) 39 | * [Further-Reading](#further-reading) 40 | 41 | 42 | Project Setup 43 | ============ 44 | 45 | Clone this GitHub repository. The [MultiSignatureWallet.sol](./contracts/MultiSignatureWallet.sol) file in the contracts directory has the structure of a multisignature wallet that you will be implementing. 46 | 47 | 48 | Implementing the Contract 49 | ============ 50 | 51 | Let’s work on this contract in Remix to get more familiar with the Remix IDE. Navigate to [Remix](https://remix.ethereum.org/) in your browser. 52 | 53 | 54 | In the upper left corner click the “+” icon to create a new file. Call it MultiSignatureWallet.sol. 55 | 56 | 57 | Copy the contents of the MultiSignatureWallet.sol file found in the project directory into Remix. 58 | 59 | ![](./gifs/remix.gif) 60 | 61 | Let’s review what this contract needs to be able to do before we start writing the code. 62 | 63 | 64 | The contract will have multiple owners that will determine which transactions the contract is allowed to execute. Contract owners need to be able to propose transactions that other owners can either confirm or revoke. If a proposed transaction receives enough support, it will be executed. 65 | 66 | 67 | Keeping these requirements in mind, let’s start going through the contract stub and start implementing this functionality. 68 | 69 | 70 | Remix is a browser based IDE that has code parsing built in, so it will show us any syntax or compilation errors directly in our environment. Notice the yellow triangles along the left side of the screen. Hovering over the triangle with your cursor will display the warning message. Yellow triangles are warning messages, whereas red circles are error messages and will prevent your contract from compiling. 71 | 72 | 73 | Constructor 74 | ===== 75 | 76 | Starting with the constructor, you can see that with the latest solidity compiler version, using the contract name as the constructor name has been deprecated, so let’s change it to constructor. 77 | 78 | ```javascript 79 | constructor(address[] memory _owners, uint _required) 80 | ``` 81 | 82 | We are going to want to check the user inputs to the constructor to make sure that a user does not require more confirmations than there are owners, that the contract requires at least one confirmation before sending a transaction and that the owner array contains at least one address. 83 | 84 | 85 | We can create a modifier that checks these conditions 86 | 87 | ```javascript 88 | modifier validRequirement(uint ownerCount, uint _required) { 89 | if (_required > ownerCount || _required == 0 || ownerCount == 0) 90 | revert(); 91 | _; 92 | } 93 | ``` 94 | 95 | 96 | 97 | And call it when the constructor runs. 98 | 99 | ```javascript 100 | constructor(address[] memory _owners, uint _required) public 101 | validRequirement(_owners.length, _required) 102 | {...} 103 | ``` 104 | 105 | We are going to want to keep the `_owners` and `_required` values for the life of the contract, so we need to declare the variables in storage in order to save them. 106 | 107 | ```javascript 108 | address[] public owners; 109 | uint public required; 110 | mapping (address => bool) public isOwner; 111 | ``` 112 | 113 | We also added a mapping of owner addresses to booleans so that we can quickly reference (without having to loop over the owners array) whether a specific address is an owner or not. 114 | 115 | 116 | All of these variables will be set in the constructor. 117 | 118 | ```javascript 119 | constructor(address[] memory _owners, uint _required) public 120 | validRequirement(_owners.length, _required) 121 | { 122 | for (uint i=0; i<_owners.length; i++) { 123 | isOwner[_owners[i]] = true; 124 | } 125 | owners = _owners; 126 | required = _required; 127 | } 128 | ``` 129 | 130 | Submit Transaction 131 | ===== 132 | 133 | The `submitTransaction` function allows an owner to submit and confirm a transaction. 134 | 135 | 136 | First we need to restrict this function to only be callable by an owner. 137 | 138 | ```javascript 139 | require(isOwner[msg.sender]); 140 | ``` 141 | 142 | Looking at the rest of the contract stub, you will notice that there are two other functions in the contract that can help you implement this function, one is called `addTransaction` that takes the same inputs as `submitTransaction` and returns a `uint transactionId`. The other is called `confirmTransaction` that takes a `uint transactionId`. 143 | 144 | 145 | We can easily implement `submitTransaction` with the help of these other functions: 146 | 147 | ```javascript 148 | function submitTransaction(address destination, uint value, bytes memory data) 149 | public 150 | returns (uint transactionId) 151 | { 152 | require(isOwner[msg.sender]); 153 | transactionId = addTransaction(destination, value, data); 154 | confirmTransaction(transactionId); 155 | } 156 | ``` 157 | 158 | Add Transaction 159 | ===== 160 | 161 | Let’s jump to the `addTransaction` function and implement that. This function adds a new transaction to the transaction mapping (which we are about to create), if the transaction does not exist yet. 162 | 163 | ```javascript 164 | function addTransaction(address destination, uint value, bytes memory data) internal returns (uint transactionId); 165 | ``` 166 | 167 | A transaction is a data structure that is defined in the contract stub. 168 | 169 | ```javascript 170 | struct Transaction { 171 | address destination; 172 | uint value; 173 | bytes data; 174 | bool executed; 175 | } 176 | ``` 177 | 178 | We need to store the inputs to the `addTransaction` function in a Transaction struct and create a transaction id for the transaction. Let’s create two more storage variables to keep track of the transaction ids and transaction mapping. 179 | 180 | ```javascript 181 | uint public transactionCount; 182 | mapping (uint => Transaction) public transactions; 183 | ``` 184 | 185 | In the `addTransaction` function we can get the transaction count, store the transaction in the mapping and increment the count. This function modifies the state so it is a good practice to emit an event. 186 | 187 | 188 | We will emit a `Submission` event that takes a `transactionId`. Let’s define the event first. Events are usually defined at the top of a Solidity contract, so that is what we will do. Add this line just below the contract declaration. 189 | 190 | ```javascript 191 | event Submission(uint indexed transactionId); 192 | ``` 193 | 194 | The ***indexed*** keyword in the event declaration makes the event easily searchable and is useful when building user interfaces that need to parse lots of events. 195 | 196 | 197 | In the function body we can call the event. 198 | 199 | ```javascript 200 | function addTransaction(address destination, uint value, bytes memory data) 201 | internal 202 | returns (uint transactionId) 203 | { 204 | transactionId = transactionCount; 205 | transactions[transactionId] = Transaction({ 206 | destination: destination, 207 | value: value, 208 | data: data, 209 | executed: false 210 | }); 211 | transactionCount += 1; 212 | emit Submission(transactionId); 213 | } 214 | ``` 215 | 216 | The `uint transactionId` is returned for the `submitTransaction` function to hand over to the `confirmTransaction` function. 217 | 218 | 219 | Confirm Transaction 220 | ===== 221 | 222 | ```javascript 223 | function confirmTransaction(uint transactionId) public {} 224 | ``` 225 | 226 | The confirm transaction function allows an owner to confirm an added transaction. 227 | 228 | 229 | This requires another storage variable, a confirmations mapping that stores a mapping of boolean values at owner addresses. This variable keeps track of which owner addresses have confirmed which transactions. 230 | 231 | ```javascript 232 | mapping (uint => mapping (address => bool)) public confirmations; 233 | ``` 234 | 235 | There are several checks that we will want to verify before we execute this transaction. First, only wallet owners should be able to call this function. Second, we will want to verify that a transaction exists at the specified `transactionId`. Last, we want to verify that the `msg.sender` has not already confirmed this transaction. 236 | 237 | ```javascript 238 | require(isOwner[msg.sender]); 239 | require(transactions[transactionId].destination != address(0)); 240 | require(confirmations[transactionId][msg.sender] == false); 241 | ``` 242 | 243 | Once the transaction receives the required number of confirmations, the transaction should execute, so once the appropriate boolean is set to true 244 | 245 | ```javascript 246 | confirmations[transactionId][msg.sender] = true; 247 | ``` 248 | 249 | Since we are modifying the state in this function, it is a good practice to log an event. First, we define the event called `Confirmation` that logs the confirmers address as well as the `transactionId` of the transaction that they are confirming. 250 | 251 | ```javascript 252 | event Confirmation(address indexed sender, uint indexed transactionId); 253 | ``` 254 | 255 | Both of these event parameters are indexed to make the event more easily searchable. Now we can call the event in the function. 256 | 257 | 258 | After logging the event we can attempt to execute the transaction 259 | 260 | ```javascript 261 | executeTransaction(transactionId); 262 | ``` 263 | 264 | So the entire function should look like this: 265 | 266 | ```javascript 267 | function confirmTransaction(uint transactionId) 268 | public 269 | { 270 | require(isOwner[msg.sender]); 271 | require(transactions[transactionId].destination != address(0)); 272 | require(confirmations[transactionId][msg.sender] == false); 273 | confirmations[transactionId][msg.sender] = true; 274 | emit Confirmation(msg.sender, transactionId); 275 | executeTransaction(transactionId); 276 | } 277 | ``` 278 | 279 | Execute Transaction 280 | ===== 281 | 282 | The execute transaction function takes a single parameter, the `transactionId`. 283 | 284 | 285 | First, we want to make sure that the Transaction at the specified id has not already been executed. 286 | 287 | ```javascript 288 | require(transactions[trandsactionId].executed == false); 289 | ``` 290 | 291 | Then we want to verify that the transaction has at least the required number of confirmations. 292 | 293 | 294 | To do this we will loop over the owners array and count how many of the owners have confirmed the transaction. If the count reaches the required amount, we can stop counting (save gas) and just say the the requirement has been reached. 295 | 296 | 297 | I define the helper function isConfirmed, which we can call from the `executeTransaction` function. 298 | 299 | ```javascript 300 | function isConfirmed(uint transactionId) 301 | public 302 | view 303 | returns (bool) 304 | { 305 | uint count = 0; 306 | for (uint i=0; i 390 | ``` 391 | 392 | Deploy the contracts 393 | ``` 394 | truffle(develop)> migrate 395 | ``` 396 | If `migrate` does not work, try `migrate --reset`. 397 | 398 | And then get the deployed instances of the SimpleStorage.sol and MultiSignatureWallet.sol contracts. 399 | 400 | ``` 401 | truffle(develop)> var ss = await SimpleStorage.at(SimpleStorage.address) 402 | truffle(develop)> var ms = await MultiSignatureWallet.at(MultiSignatureWallet.address) 403 | ``` 404 | 405 | Check the state of the the SimpleStorage contract 406 | 407 | ``` 408 | truffle(develop)> ss.storedData.call() 409 | 410 | ``` 411 | 412 | This means that it is 0. You can verify by waiting for the promise to resolve and converting the answer to a string. Try it with: 413 | 414 | ``` 415 | ss.storedData.call().then(res => { console.log( res.toString(10) )} ) 416 | 0 417 | ``` 418 | 419 | Let’s submit a transaction to update the state of the SimpleStorage contract to the MultiSig contract. `SumbitTransaction` takes the address of the destination contract, the value to send with the transaction and the transaction data, which includes the encoded function signature and input parameters. 420 | 421 | 422 | If we want to update the SimpleStorage contract data to be 5, the encoded function signature and input parameters would look like this: 423 | 424 | ```javascript 425 | var encoded = '0x60fe47b10000000000000000000000000000000000000000000000000000000000000005' 426 | ``` 427 | 428 | Let's get the available accounts and then make a call to the MultiSig contract: 429 | 430 | ``` 431 | truffle(develop)> var accounts = await web3.eth.getAccounts() 432 | truffle(develop)> ms.submitTransaction(ss.address, 0, encoded, {from: accounts[0]}) 433 | ``` 434 | 435 | And we see the transaction information printed in the terminal window. In the logs, we can see that a “Submission” event was fired, as well as a “Confirmation” event, which is what we expect. 436 | 437 | 438 | The current state of the MultiSig has one transaction that has not been executed and has one confirmation (from the address that submitted it). One more confirmation should cause the transaction to execute. Let’s use the second account to confirm it. The `confirmTransaction` function takes one input, the index of the Transaction to confirm. 439 | 440 | ``` 441 | truffle(develop)> ms.confirmTransaction(0, {from: accounts[1]}) 442 | ``` 443 | 444 | The transaction information is printed in the terminal. You should see two log events this time as well. A “Confirmation” event as well as an “Execution” event. This indicates that the call to SimpleStorage executed successfully. If it didn’t execute successfully, we would see an “ExecutionFailure” event there instead. 445 | 446 | 447 | We can verify that the state of the contract was updated by running 448 | 449 | ``` 450 | truffle(develop)> ss.storedData.call() 451 | 452 | ``` 453 | 454 | The `storedData` is now 5. And we can check that the address that updated the SimpleStorage contract was the MultiSig Wallet. 455 | 456 | ``` 457 | truffle(develop)> ss.caller.call() 458 | '0x855d1c79ad3fb086d516554dc7187e3fdfc1c79a' 459 | truffle(develop)> ms.address 460 | '0x855d1c79ad3fb086d516554dc7187e3fdfc1c79a' 461 | ``` 462 | 463 | The two addresses are the same! 464 | 465 | Further Reading 466 | ============ 467 | - [ConsenSys MultiSig Repo](https://medium.com/hellogold/ethereum-multi-signature-wallets-77ab926ab63b) 468 | - [List of MultiSig Contracts on Mainnet](https://medium.com/talo-protocol/list-of-multisig-wallet-smart-contracts-on-ethereum-3824d528b95e) 469 | - [What is a MultiSig Wallet?](https://medium.com/hellogold/ethereum-multi-signature-wallets-77ab926ab63b) 470 | - [Unchained Capital's Multi-Sig](https://blog.unchained-capital.com/a-simple-safe-multisig-ethereum-smart-contract-for-hardware-wallets-a107bd90bb52) 471 | - [Grid+: Toward an Ethereum MultiSig Standard](https://blog.gridplus.io/toward-an-ethereum-multisig-standard-c566c7b7a3f6) 472 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/MultiSignatureWallet.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract MultiSignatureWallet { 4 | 5 | struct Transaction { 6 | bool executed; 7 | address destination; 8 | uint value; 9 | bytes data; 10 | } 11 | 12 | event Deposit(address indexed sender, uint value); 13 | 14 | /// @dev Fallback function allows to deposit ether. 15 | function() 16 | external 17 | payable 18 | { 19 | if (msg.value > 0) { 20 | emit Deposit(msg.sender, msg.value); 21 | } 22 | } 23 | 24 | /* 25 | * Public functions 26 | */ 27 | /// @dev Contract constructor sets initial owners and required number of confirmations. 28 | /// @param _owners List of initial owners. 29 | /// @param _required Number of required confirmations. 30 | constructor(address[] memory _owners, uint _required) public {} 31 | 32 | /// @dev Allows an owner to submit and confirm a transaction. 33 | /// @param destination Transaction target address. 34 | /// @param value Transaction ether value. 35 | /// @param data Transaction data payload. 36 | /// @return Returns transaction ID. 37 | function submitTransaction(address destination, uint value, bytes memory data) public returns (uint transactionId) {} 38 | 39 | /// @dev Allows an owner to confirm a transaction. 40 | /// @param transactionId Transaction ID. 41 | function confirmTransaction(uint transactionId) public {} 42 | 43 | /// @dev Allows an owner to revoke a confirmation for a transaction. 44 | /// @param transactionId Transaction ID. 45 | function revokeConfirmation(uint transactionId) public {} 46 | 47 | /// @dev Allows anyone to execute a confirmed transaction. 48 | /// @param transactionId Transaction ID. 49 | function executeTransaction(uint transactionId) public {} 50 | 51 | /* 52 | * (Possible) Helper Functions 53 | */ 54 | /// @dev Returns the confirmation status of a transaction. 55 | /// @param transactionId Transaction ID. 56 | /// @return Confirmation status. 57 | function isConfirmed(uint transactionId) internal view returns (bool) {} 58 | 59 | /// @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet. 60 | /// @param destination Transaction target address. 61 | /// @param value Transaction ether value. 62 | /// @param data Transaction data payload. 63 | /// @return Returns transaction ID. 64 | function addTransaction(address destination, uint value, bytes memory data) internal returns (uint transactionId) {} 65 | } 66 | -------------------------------------------------------------------------------- /contracts/SimpleStorage.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract SimpleStorage { 4 | uint public storedData; 5 | 6 | address public caller; 7 | 8 | function set(uint x) public { 9 | caller = msg.sender; 10 | storedData = x; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gifs/remix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConsenSys-Academy/multisig-wallet-exercise/197758988e4e69e72c2aa46a606eb94391da2bc1/gifs/remix.gif -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var MultiSig = artifacts.require("MultiSignatureWallet") 2 | var SimpleStorage = artifacts.require("SimpleStorage") 3 | 4 | module.exports = function(deployer, network, accounts) { 5 | 6 | const owners = [accounts[0], accounts[1]] 7 | 8 | deployer.deploy(SimpleStorage) 9 | deployer.deploy(MultiSig, owners, 2) 10 | }; 11 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8545, 6 | network_id: "*" // Match any network id 7 | } 8 | } 9 | }; 10 | --------------------------------------------------------------------------------