├── .gitignore ├── LICENSE ├── README.md ├── dex ├── LiquidityToken.sol ├── MockERC20.sol ├── README.md ├── SimpleDex.sol └── img │ └── x*y=k.png ├── geth └── .gitkeep ├── part1 ├── README.md ├── Storage.abi ├── Storage.bin ├── Storage.sol └── genesis.json ├── part2 ├── README.md └── Token.sol ├── part3 └── README.md ├── part4 ├── README.md ├── RoyaltyStandard.sol ├── SimpleMarketplace.sol └── SimpleNFT.sol ├── part5 ├── MemberList.sol ├── README.md ├── Relayer.sol ├── img │ └── architecture.png ├── package-lock.json ├── package.json └── sign.js └── part6 ├── BankV1.sol ├── BankV2.sol ├── Proxy.sol ├── README.md ├── UpgradeProxy.sol ├── calldata.js ├── img ├── v1.png └── v2.png ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # part1 107 | geth/ 108 | keystore/ 109 | password.txt 110 | 111 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidity-training 2 | スマートコントラクト学習用のリポジトリ 3 | 4 | # 学習のマイルストーン 5 | ## 初級 6 | 1. [ブロックチェーンを立ち上げて、コントラクトを動かしてみよう](./part1) 7 | 2. [Tokenコントラクトを作ってみよう](./part2) 8 | 3. [Tokenを配布してみよう](./part3) 9 | 10 | ## 中級 11 | 4. [ロイヤリティスタンダードに対応したNFTマーケットプレイスを作ってみよう](./part4) 12 | 5. [ガス代を肩代わりするmeta transactionのRelayerを作ってみる](./part5) 13 | 6. [Upgrade可能なコントラクトを作ってみる](./part6) 14 | 15 | # 実践編 16 | - [SimpleなDEXを作ってみる](./dex) 17 | -------------------------------------------------------------------------------- /dex/LiquidityToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | pragma solidity >=0.7.0 <0.9.0; 3 | 4 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC20/IERC20.sol"; 5 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract LiquidityToken is ERC20 { 8 | 9 | address public dex; 10 | 11 | // このLPトークンのトークンペア 12 | address public token0; 13 | address public token1; 14 | 15 | uint112 private reserve0; // uses single storage slot, accessible via getReserves 16 | uint112 private reserve1; // uses single storage slot, accessible via getReserves 17 | 18 | event Mint(address indexed sender, uint amount0, uint amount1); 19 | event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); 20 | event Swap( 21 | address indexed sender, 22 | uint amount0In, 23 | uint amount1In, 24 | uint amount0Out, 25 | uint amount1Out, 26 | address indexed to 27 | ); 28 | 29 | constructor() ERC20("LiquidityToken", "LPT") { 30 | dex = msg.sender; 31 | } 32 | 33 | function initialize(address _token0, address _token1) public { 34 | require(msg.sender == dex, "unauthorized. only dex can call"); 35 | token0 = _token0; 36 | token1 = _token1; 37 | } 38 | 39 | function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1) { 40 | _reserve0 = reserve0; 41 | _reserve1 = reserve1; 42 | } 43 | 44 | function mint(address to) public returns (uint liquidity) { 45 | uint balance0 = IERC20(token0).balanceOf(address(this)); 46 | uint balance1 = IERC20(token1).balanceOf(address(this)); 47 | uint amount0 = balance0 - reserve0; 48 | uint amount1 = balance1 - reserve1; 49 | 50 | uint totalSupply = totalSupply(); 51 | if (totalSupply == 0) { 52 | // 新規にLPトークンを発行する場合 53 | // 両者を掛けたものの二乗根 54 | liquidity = sqrt(amount0 * amount1); 55 | } else { 56 | // 追加で発行する場合 57 | // 新たに供給される流動性の量と、供給されている流動性の比率にLPトークンの総量を掛ける 58 | // minの両式は基本的に同じ値を返すが、少数以下の演算で誤差が生じる場合があるためか最小値を取る形にしている 59 | liquidity = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1); 60 | } 61 | require(liquidity > 0, "insufficient liquidity minted"); 62 | 63 | // LPトークンを発行 64 | _mint(to, liquidity); 65 | 66 | // Reservesを更新 67 | _update(balance0, balance1); 68 | 69 | emit Mint(msg.sender, amount0, amount1); 70 | } 71 | 72 | function burn(address to) public returns (uint amount0, uint amount1) { 73 | uint balance0 = IERC20(token0).balanceOf(address(this)); 74 | uint balance1 = IERC20(token1).balanceOf(address(this)); 75 | uint liquidity = balanceOf(address(this)); 76 | uint totalSupply = totalSupply(); 77 | 78 | amount0 = liquidity * balance0 / totalSupply; // using balances ensures pro-rata distribution 79 | amount1 = liquidity * balance1 / totalSupply; // using balances ensures pro-rata distribution 80 | require(amount0 > 0 && amount1 > 0, "insufficinet liquidity burned"); 81 | 82 | // LPトークンをBurn 83 | _burn(address(this), liquidity); 84 | IERC20(token0).transfer(to, amount0); 85 | IERC20(token1).transfer(to, amount1); 86 | 87 | balance0 = IERC20(token0).balanceOf(address(this)); 88 | balance1 = IERC20(token1).balanceOf(address(this)); 89 | 90 | _update(balance0, balance1); 91 | 92 | emit Burn(msg.sender, amount0, amount1, to); 93 | } 94 | 95 | function swap(uint amount0Out, uint amount1Out, address to) public { 96 | require(amount0Out > 0 || amount1Out > 0, "insufficient output amount"); 97 | require(amount0Out < reserve0 && amount1Out < reserve1, "insufficient liquidity"); 98 | require(to != token0 && to != token1, "invalid to"); 99 | 100 | if (amount0Out > 0) IERC20(token0).transfer(to, amount0Out); 101 | if (amount1Out > 0) IERC20(token1).transfer(to, amount1Out); 102 | 103 | uint balance0 = IERC20(token0).balanceOf(address(this)); 104 | uint balance1 = IERC20(token1).balanceOf(address(this)); 105 | 106 | uint amount0In = balance0 > reserve0 - amount0Out ? balance0 - (reserve0 - amount0Out) : 0; 107 | uint amount1In = balance1 > reserve1 - amount1Out ? balance1 - (reserve1 - amount1Out) : 0; 108 | require(amount0In > 0 || amount1In > 0, "insufficinet input amount"); 109 | 110 | _update(balance0, balance1); 111 | 112 | emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); 113 | } 114 | 115 | function _update(uint balance0, uint balance1) private { 116 | reserve0 = uint112(balance0); 117 | reserve1 = uint112(balance1); 118 | } 119 | 120 | 121 | function min(uint x, uint y) internal pure returns (uint z) { 122 | z = x < y ? x : y; 123 | } 124 | 125 | // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) 126 | function sqrt(uint y) internal pure returns (uint z) { 127 | if (y > 3) { 128 | z = y; 129 | uint x = y / 2 + 1; 130 | while (x < z) { 131 | z = x; 132 | x = (y / x + x) / 2; 133 | } 134 | } else if (y != 0) { 135 | z = 1; 136 | } 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /dex/MockERC20.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.7.0 <0.9.0; 3 | 4 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor(string memory name, string memory symbol) payable ERC20(name, symbol) { 8 | _mint(_msgSender(), 1_000_000); 9 | } 10 | 11 | function mint(address account, uint256 amount) public { 12 | _mint(account, amount); 13 | } 14 | 15 | // decimalは”0”にしておく 16 | function decimals() public pure override returns (uint8) { 17 | return 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /dex/README.md: -------------------------------------------------------------------------------- 1 | # SimpleなDEXを作ってみる 2 | 3 | ## Steps 4 | 1. DEXとは? 5 | 2. AMMとは? 6 | 3. AMMの仕組み 7 | 4. DEXコントラクトの関数 8 | 5. DEXの動作確認 9 | 10 | ## 1. DEXとは? 11 | Decentralizedな暗号資産の取引所のことです。有名どころでは、UniswapやPancakeswap、0xなどが挙げられます。DEXと対を成すのが、CEXで、Centralizedな取引所のことです。有名どころでは、BitflyerやCoincheck、Binanceが挙げられます。 12 | DEXには多様な方式があります。DEXが現れた当初は、証券取引の板取引のようなOrderBookがあって、買い手と売り手をマッチングさせるようなものが主流でした。しかし、UniswapやPancakeswapのようなAMMと呼ばれる手法が登場すると、DEXの主流に取ってかわりました。 13 | 14 | ## 2. AMMとは? 15 | AMMはAutomated Market Makerの略で、売り手に当たるものが自動化されることが最大の特徴です。通常の取引を考えた場合、買い手一人では売買は成立せず、売り手の存在が不可欠です。買い手は、自分の満足する価格の売り手が現れるのを待つ必要があります。一方、AMMでは、売り手が自動化されているので、買い手はいつでも買いたいときに買いたい分だけ、暗号資産を購入できます。この点が革命的と言えます。 16 | 例えば、BitcoinやEthereumのような人気の高い暗号資産の場合、買い手も売り手も十分数存在するので、買いたいときにいつで買える状態が作られます。一方、名前の知られていないマイナーな暗号資産の場合、買い手も売り手も不足しているので、トレードがなかなか成立しないという事態が発生します。このようなケースに光明を差し込むのがAMMと言えます。 17 | 18 | 19 | ## 3. AMMの仕組み 20 | 簡単に説明すると2ステップに分かれます。 21 | 22 | AMMでは売り手がいなくても、トレードが成立しますが、その原資はどこから来るかというと、複数の第三者から提供されて、Poolされています。例えば、ETHとDAIに交換したい場合は、ETHとDAIの両方を持ってる第三者が提供してPoolします。第三者が資産を提供するメリットは、売買の手数料報酬を得ることです。 23 | 24 | 次に、買い手は購入したい量を指定します。すると、価格が自動的に決まります。この価格が決まるメカニズムは、需要と供給により価格が決まる仕組みと同じです。例えば、ETHからDAIに交換する人が増えた場合、いいかえると、DAIの需要が高い場合は、PoolされていDAIの量が少なくなります。量が少なくなるということはDAIの方が希少なので、DAIの価格が高くなり、ETHの価格が下落します。このように、需要と供給、Poolされている資産の割合の増減によって価格が決まります。 25 | 26 | ### 流動性の供給 27 | 最初の原資をPoolすることを`流動性を供給する`とAMM的に表現します。前述のマイナーな通貨で売り手と買い手が少なくトレードが成立しない状態を`流動性がない`と呼びます。AMMによって`流動性を作る`ことで売買を成立させます。 28 | 流動性を供給すると流動性トークン(`LiquidityToken`)がMintされます。この流動性トークンの量は、供給した流動性の量に比例します。もし、供給した流動性を手元に戻したい場合は、この流動性トークンをBurnします。 29 | 30 | #### 具体例:Mint 31 | 具体的にETHとDAIを流動性として供給する場合を考えてみます。 32 | ここで、ETHはそのままETHではなく、ERC20でWrapedしたTokenの形のWraped ETH(WETH)として表現します。 33 | 最初にWETHを1枚、DAIを100,000枚を供給するとすると、Mintされる流動性トークン(LPトークン)の量はこのように計算されます。 34 | ```sh 35 | sqrt(WETH * DAI) = LP Token 36 | sqrt(1 * 100,000) = 316 37 | ``` 38 | 両者を掛けたものの二乗根を取り、316枚のLPトークンがMintされます。 39 | 40 | 次に、WETHを2枚、DAIを100,000枚を供給したいとします。 41 | しかし、WETHは1枚しか供給できません。WETH2枚を供給したいならが、DAIを追加で100,000枚用意する必要があります。 42 | 理由は、流動性プールの通貨比率が一定に保たれるからです。 43 | ```sh 44 | additional WETH * reserved DAI / reserved WETH = required DAI 45 | 2 * 100,000 / 1 = 200,000 46 | ``` 47 | ということで、実際に供給される流動性はWETHが1枚とDAIが100,000枚づつです。 48 | Mintされる流動性トークンの量は、新たに供給される流動性の量と、供給されている流動性の比率にLPトークンの総量を掛けたものになります。 49 | ```sh 50 | total LP Supply * additional DAI / reserved DAI = additional LP Token 51 | 316 * 100,000 / 100,000 = 316 52 | ``` 53 | 新規に流動性を供給したときと同じく316枚がMintされます。 54 | 55 | #### 具体例:Burn 56 | 続いて、Burnする場合を考えてみます。 57 | 先ほど追加で供給した316枚のLPトークンをBurnすることを考えます。 58 | すると、返却されるWETHとDAIの量は、burnされるLPトークンとLPトークンの総量の比率に供給されているトークン量を掛けたものにないrます。 59 | ```sh 60 | # WETH 61 | reserved WETH * burn LP token / total LP Supply = Backed WETH 62 | 2 * 316 / 632 = 1 63 | 64 | # DAI 65 | reserved DAI * burn LP token / total LP Supply = DAi WETH 66 | 200,000 * 316 / 632 = 100,000 67 | ``` 68 | それぞれ、WETHが1枚、DAIが100,000枚、返却されます。 69 | 70 | ### 価格の決定 71 | AMMでの価格決定の方法は有名な`k = x * y`という式よって表されます。`x`と`y`がそれぞれ供給された流動性の量を表します。`k`は定数で、一定の値です。流動性の量の積は一定に保たれるということを表しますが、この式を変形するとわかりやすいです。 これを変形すると、`y = k / x`でお馴染み、反比例の式が姿を表します。流動性Xが増えると、反比例する形で流動性yが減少します。 72 | 73 | 具体的にWETHとDAIの取引を例に、X軸をETHの供給量、y軸をDAIの供給量としてグラフを引いてみます。 74 | 75 | ![x*y=k.png](./img/x*y=k.png) 76 | 77 | 点Aを初期状態を考えます。点AではETHが1枚、DAIが100,000枚供給されています。これは、1枚のETHと10万枚のDAIの価値が釣り合っている状態です。言い換えると、ETH1枚が10万DAIに値するので、つまり、ETHの価格は10万DAIということです。グラフの傾きが価格を表します。 78 | 79 | ここから、ETHが売られて、DAIが買われる動きが加速したとします。直感的には、DAIの価値が高い状況なので、ETHの価格が下落します。グラフで表すと、点Aから点Bへ移行したとします。ETHの供給量は2倍に増え、DAIの供給量は半分に減りました。2枚のETHと5万枚のDAIが同じ価値なので、ETHの価格は、2万5千DAIです。確かに、ETHの価格が下落していることがわかります。グラフの傾きを見れば明白です。 80 | 81 | 82 | ## 4. DEXコントラクトの関数 83 | AMMの仕組みがわかったところで、より具体的にDEXコントラクトの関数について解説します。 84 | 85 | 本実装では主に2つのコントラクトが存在します。1つ目の[SimpleDex.sol](./SimpleDex.sol)がDEXに相当するコントラクトで、2つ目の[LiquidityToken.sol](./LiquidityToken.sol)が流動性トークンに相当するコントラクトです。流動性トークンについては、ほぼERC20と同じです。ただ、`swap`関数が実装されている点が特徴的です。また、mint時に供給された流動性分のトークンを発行したり、burn時に同等量の流動性を返却したりと、流動性トークンらしい実装が為されています。 86 | 87 | DEXコントラクトの主要な関数はこちらです。 88 | - `addLiquidity`: 流動性を供給する(Mint) 89 | - `removeLiquidity`: 流動性を引き抜く(Burn) 90 | - `swap`: 暗号資産とトレードする 91 | 92 | 93 | ## 5. DEXの動作確認 94 | それでは、実際にコントラクトをデプロイして、一連の関数の動作確認をしてみます。 95 | 96 | 1. コントラクトのデプロイ 97 | - WETHとDAIに当たる2つの[ERC20](./MockERC20.sol)をデプロイします。 98 | - Deployerにそれぞれ、1億をmintします。 99 | - [Dexコントラクト](./SimpleDex.sol)をデプロイします。 100 | 101 | 3. 流動性の供給 102 | - mintした全量をDEXコントラクトに対してApproveします。 103 | - 初期の流動性としてWETHを10枚、DAIを100万供給します。つまり、ETHの価格が10万DAIの場合です。 104 | - LPトークンが`3162`枚発行されます 105 | 106 | 4. トレード 107 | - 別のアカウントにETHをmintします 108 | - DEXコントラクトをApproveします 109 | - 1WETHを売ってDAIを買うトレードを行います。 110 | - 手数料が徴収されて`90661`DAIを受け取ります 111 | - 全く同じトレードをもう一度行います。 112 | - すると、`75569`DAIを受けとりました。ETHの価格が下落していることがわかります。 113 | 114 | 5. 流動性の引き抜き 115 | - DEXコントラクトに対して、LPトークンをApproveします 116 | - 半分の流動性(`1581`)を引き抜きます。 117 | - `6`WETHと`416885`DAIを受け取ります。 118 | -------------------------------------------------------------------------------- /dex/SimpleDex.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | pragma solidity >=0.7.0 <0.9.0; 3 | 4 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC20/IERC20.sol"; 5 | import "./LiquidityToken.sol"; 6 | 7 | contract SimpleDEX { 8 | // LPトークンのmap 9 | mapping(address => mapping(address => address)) public lptokens; 10 | 11 | event PairCreated(address indexed token0, address indexed token1, address lptoken); 12 | 13 | function addLiquidity( 14 | address tokenA, 15 | address tokenB, 16 | uint amountADesired, 17 | uint amountBDesired, 18 | address to 19 | ) public returns (uint amountA, uint amountB, uint liquidity) { 20 | // ソート 21 | // 例えば、初回にtokenA=hoge,tokenB=fugaが指定されて、2回目にtokenA=fuga,tokenB=hogeが指定されても、いいように。 22 | (address token0, address token1) = sortTokens(tokenA, tokenB); 23 | (uint amount0Desired, uint amount1Desired) = sortAmounts(tokenA, tokenB, amountADesired, amountBDesired); 24 | 25 | // Liquidityを追加する 26 | (address lptoken, uint amount0, uint amount1) = _addLiquidity(token0, token1, amount0Desired, amount1Desired); 27 | (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); 28 | 29 | // LPトークンをmintする 30 | liquidity = LiquidityToken(lptoken).mint(to); 31 | } 32 | 33 | function removeLiquidity( 34 | address tokenA, 35 | address tokenB, 36 | uint liquidity, 37 | address to 38 | ) public returns (uint amountA, uint amountB) { 39 | (address token0, address token1) = sortTokens(tokenA, tokenB); 40 | address lptoken = lptokens[token0][token1]; 41 | 42 | // burnする分のLPトークンをLPコントラクトにデポジット 43 | LiquidityToken(lptoken).transferFrom(msg.sender, lptoken, liquidity); 44 | 45 | // LPトークンをburn 46 | (uint amount0, uint amount1) = LiquidityToken(lptoken).burn(to); 47 | (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); 48 | } 49 | 50 | function swap( 51 | uint amountIn, 52 | bool swapsTokenA, 53 | address tokenA, 54 | address tokenB, 55 | address to 56 | ) public returns (uint amountOut) { 57 | require(amountIn > 0, "insufficinet input amount"); 58 | 59 | (address token0, address token1) = sortTokens(tokenA, tokenB); 60 | address lptoken = lptokens[token0][token1]; 61 | 62 | { // ローカル変数が多いと”stack too deep”になるので、避ける 63 | (uint reserve0, uint reserve1) = LiquidityToken(lptoken).getReserves(); 64 | (uint reserveIn, uint reserveOut) = (tokenA == token0 && swapsTokenA) || (tokenA != token0 && !swapsTokenA) ? (reserve0, reserve1) : (reserve1, reserve0); 65 | // 供給する流動性の内、0.3%分は手数料として徴収される 66 | uint amountInWithFee = amountIn * 997; 67 | amountOut = amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee); 68 | } 69 | 70 | if (swapsTokenA) IERC20(tokenA).transferFrom(msg.sender, lptoken, amountIn); 71 | else IERC20(tokenB).transferFrom(msg.sender, lptoken, amountIn); 72 | 73 | (uint amountAOut, uint amountBOut) = swapsTokenA ? (uint(0), amountOut) : (amountOut, uint(0)); 74 | (uint amount0Out, uint amount1Out) = sortAmounts(tokenA, tokenB, amountAOut, amountBOut); 75 | LiquidityToken(lptoken).swap(amount0Out, amount1Out, to); 76 | } 77 | 78 | function sortAmounts(address tokenA, address tokenB, uint amountA, uint amountB) internal pure returns (uint amount0, uint amount1) { 79 | require(amountA > 0 || amountB > 0, "either amount is zero"); 80 | (amount0, amount1) = tokenA < tokenB ? (amountA, amountB) : (amountB, amountA); 81 | } 82 | 83 | function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { 84 | require(tokenA != tokenB, "both tokens are same address"); 85 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 86 | // もう片方のチェックはしなくてもいい。ソートしているので 87 | require(token0 != address(0), "one of token is zero address"); 88 | } 89 | 90 | function _addLiquidity( 91 | address token0, 92 | address token1, 93 | uint amount0Desired, 94 | uint amount1Desired 95 | ) private returns (address lptoken, uint amount0, uint amount1) { 96 | // もし、LPトークンが存在しなければ、新規作成 97 | lptoken = lptokens[token0][token1]; 98 | if (lptoken == address(0)) { 99 | lptoken = createPair(token0, token1); 100 | } 101 | 102 | // LPトークンが持っている残高を取得 103 | (uint reserve0, uint reserve1) = LiquidityToken(lptoken).getReserves(); 104 | 105 | if (reserve0 == 0 && reserve1 == 0) { 106 | // 残高ゼロの場合は、指定された量を(主に、新規でLPトークンを作った場合) 107 | (amount0, amount1) = (amount0Desired, amount1Desired); 108 | } else if (amount0Desired > 0) { 109 | // token0のamountが指定されたら 110 | amount0 = amount0Desired; 111 | amount1 = amount0 * reserve1 / reserve0; 112 | } else if (amount1Desired > 0) { 113 | // token1のamountが指定されたら 114 | amount1 = amount1Desired; 115 | amount0 = amount1 * reserve0 / reserve1; 116 | } else { 117 | revert("no desired amount"); 118 | } 119 | 120 | // Liquidityを追加する 121 | IERC20(token0).transferFrom(msg.sender, lptoken, amount0); 122 | IERC20(token1).transferFrom(msg.sender, lptoken, amount1); 123 | } 124 | 125 | function createPair(address token0, address token1) internal returns (address lptoken) { 126 | require(lptokens[token0][token1] == address(0), "already exist"); 127 | 128 | // tokenペアのアドレスから決定的にLPトークンを作るために`create2`を使う 129 | // もし、”new”で生成してしまうと、実行者によってアドレスが変わってしまう 130 | bytes memory bytecode = type(LiquidityToken).creationCode; 131 | bytes32 salt = keccak256(abi.encodePacked(token0, token1)); 132 | assembly { 133 | lptoken := create2(0, add(bytecode, 32), mload(bytecode), salt) 134 | } 135 | 136 | // LPトークンにトークンペアを登録 137 | LiquidityToken(lptoken).initialize(token0, token1); 138 | lptokens[token0][token1] = lptoken; 139 | 140 | emit PairCreated(token0, token1, lptoken); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /dex/img/x*y=k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openreachtech/solidity-training/bb95d201d26eb0726d691e4ec21c3757beb094a3/dex/img/x*y=k.png -------------------------------------------------------------------------------- /geth/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openreachtech/solidity-training/bb95d201d26eb0726d691e4ec21c3757beb094a3/geth/.gitkeep -------------------------------------------------------------------------------- /part1/README.md: -------------------------------------------------------------------------------- 1 | # ブロックチェーンを立ち上げて、コントラクトを動かしてみよう 2 | 3 | ## Steps 4 | 1. ブロックチェーンを起動 5 | 2. 送金トランザクション 6 | 3. 簡単なコントラクトの開発 7 | 4. コントラクトのデプロイ 8 | 5. コントラクトの実行 9 | 10 | ## 1. ブロックチェーンを起動 11 | Solidityが書けるエンジニアではなく、ブロックチェーンエンジニアになるために、まずはブロックチェーンを起動できるようになります。 12 | まずは、GethというEthereumのコントラクトを実行するプログラムを、[公式サイト](https://geth.ethereum.org/downloads)からダウンロードします。 13 | 最新バージョンではなく、v1.13をダウンロードしてください。 14 | [The Merge](https://geth.ethereum.org/docs/interface/merge)という大型アップデート以降、Gethを単体で動かす機能が制限されるようになりました。そのため、古いバージョンである必要があります。今日のEthereumはGethとコンセンサスを行う2つのプログラムで稼働します。今回は単純化のため、Gethのみを使用します。 15 | 16 | 解答した中身にgethというプログラムが入っています。実行環境で動作するか確認します。 17 | ```sh 18 | # gethがインストールされたか確認 19 | geth version 20 | ``` 21 | 22 | 23 | ローカルでのプライベートなEthereumネットワークをプライベートネットといいます。 24 | プライベートネットを起動するにはマイナーのアカウントが必要です。インストールしたgethを使って作ります。 25 | ```sh 26 | # - パスワードの入力を求められるので、任意の値を指定します。 27 | # - アドレスが表示されるので、メモしておきます 28 | geth --datadir . account new 29 | # --- output --- 30 | Password: 12345 31 | Repeat password: 12345 32 | 33 | Your new key was generated 34 | 35 | Public address of the key: 0x77497Dc42E9C55AB9503135b7cbe9e1830895235 # このアドレス! 36 | Path of the secret key file: keystore/UTC--2024-08-03T11-36-06.056219000Z--77497dc42e9c55ab9503135b7cbe9e1830895235 37 | 38 | - You can share your public address with anyone. Others need it to interact with you. 39 | - You must NEVER share the secret key with anyone! The key controls access to your funds! 40 | - You must BACKUP your key file! Without the key, it's impossible to access account funds! 41 | - You must REMEMBER your password! Without the password, it's impossible to decrypt the key! 42 | ``` 43 | ブロックチェーンのアカウントは秘密鍵で表現されます。生成された秘密鍵は`keystore`フォルダ配下にJSON形式のファイルとして入っています。 44 | この秘密鍵に対応する公開鍵から生成された文字列がアドレスです。上のアウトプットの`Public address of the key` に続くランダムな文字列がアドレスです。 45 | ブロックチェーンではアカウントをこのアドレスで識別します。 46 | 47 | 次に、このアドレスに初期デポジットとして100ETHが付与されるように設定します。 48 | `genesis.json`を編集して、`[miner address]`を生成されたアドレスで置き換えてください。先頭の`0x`を抜いた形とし、2箇所入れ替えます。 49 | このファイルにチェーンを立ち上げる時の設定を記載します。 50 | ```json 51 | { 52 | "extradata": "0x0000000000000000000000000000000000000000000000000000000000000000[miner address]0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 53 | "alloc": { 54 | "0x[miner address]": { 55 | "__name__": "Miner Account", 56 | "balance": "0x64" 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ブロックチェーンを初期化します。 63 | ```sh 64 | # 初期化 65 | geth --datadir ./ init ./genesis.json 66 | ``` 67 | パスワードファイルを作ります。 68 | ```sh 69 | echo 123455 > ./password.txt 70 | ``` 71 | 起動します。 72 | ```sh 73 | # 起動 74 | geth --datadir ./ --mine --miner.etherbase 0x[miner address] --unlock 0 --password ./password.txt 75 | ``` 76 | 77 | 起動すると`geth.ipc`という名前のファイルが作られます。これはUNIXドメインソケットです。 78 | このファイルディスクリプタを通じてGethと通信します。 79 | 別のターミナルを開いて、Gethに接続します。 80 | ```sh 81 | geth attach geth.ipc 82 | # --- output --- 83 | # アカウントを表示します。最初に作ったアドレスが表示されるはずです 84 | > eth.accounts 85 | ["0x77497Dc42E9C55AB9503135b7cbe9e1830895235"] 86 | 87 | # ブロック高さを確認できます 88 | > eth.blockNumber 89 | 3 90 | 91 | ``` 92 | 93 | ## 2. 送金トランザクション 94 | プライベートチェーンでETHを送ってみます。 95 | 別のコーンソールを開いて、相手のアカウントを作ります。 96 | ```sh 97 | geth --datadir . account new 98 | # --- output --- 99 | INFO [08-16|11:48:18.836] Maximum peer count ETH=50 LES=0 total=50 100 | Your new account is locked with a password. Please give a password. Do not forget this password. 101 | Password: 12345 102 | Repeat password: 12345 103 | 104 | Your new key was generated 105 | 106 | Public address of the key: 0xe4b1DEfd7E585f0fce7B96B7Af154DC2CDFf21aa # 相手のアドレスをメモ 107 | Path of the secret key file: keystore/UTC--2022-08-16T04-48-20.146220000Z--e4b1defd7e585f0fce7b96b7af154dc2cdff21aa 108 | 109 | - You can share your public address with anyone. Others need it to interact with you. 110 | - You must NEVER share the secret key with anyone! The key controls access to your funds! 111 | - You must BACKUP your key file! Without the key, it's impossible to access account funds! 112 | - You must REMEMBER your password! Without the password, it's impossible to decrypt the key! 113 | ``` 114 | 115 | Gethに接続されたコンソールに戻ります。 116 | ```sh 117 | # まずは、自分のアカウントバランスを確認します。 118 | > eth.getBalance(eth.accounts[0]) 119 | 100000000000000000000 120 | 121 | # 相手のアカウントバランスは0です 122 | > eth.getBalance("0xe4b1DEfd7E585f0fce7B96B7Af154DC2CDFf21aa") 123 | 0 124 | 125 | # 1ETH、送ります 126 | > eth.sendTransaction({from: eth.accounts[0], to: "0xe4b1DEfd7E585f0fce7B96B7Af154DC2CDFf21aa", value: web3.toWei(1,"ether")}) 127 | "0x85c6d69c17f420803eac40d2449b6e09ecb1167f74d343df19a5dbada527f82f" # これは送金トランザクションのトランザクションハッシュです 128 | 129 | # 相手のバランスが更新されます 130 | > eth.getBalance("0xe4b1DEfd7E585f0fce7B96B7Af154DC2CDFf21aa") 131 | 1000000000000000000 # weiという単位で表示されます。1ETH=10**18 weiなので、0が18個並んでいます。 132 | ``` 133 | 134 | ## 3. 簡単なコントラクトを開発 135 | データを入れて、表示するだけの簡単なコントラクトを開発します。 136 | [Storage.sol](./Storage.sol) 137 | 138 | 開発したコントラクトをコンパイルします。 139 | まずはコンパイラーをインストールします。 140 | ```sh 141 | # インストール 142 | npm install -g solc 143 | 144 | # 確認 145 | solc --version 146 | ``` 147 | [Installing the Solidity Compiler](https://docs.soliditylang.org/en/v0.8.16/installing-solidity.html#npm-node-js) 148 | 149 | コンパイルします。 150 | ```sh 151 | solc --output-dir ./ --bin --abi --overwrite Storage.sol 152 | ``` 153 | currentディレクトリに`bin`と`abi`ファイルが作られます。 154 | `bin`がコンパイルされたバイナリファイルです。 155 | `abi`の方が、コントラクトを実行するためのI/Oが定義されたファイルです。 156 | 157 | ## 4. コントラクトのデプロイ 158 | プライベートネットにStorageコントラクトをデプロイします。 159 | Gethに接続しているターミナルを開きます。 160 | ```sh 161 | # eth.contract(<ここにStorage.abiの中身を入れます>) 162 | storage = eth.contract([{"inputs":[],"name":"retrieve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"num","type":"uint256"}],"name":"store","outputs":[],"stateMutability":"nonpayable","type":"function"}]) 163 | 164 | # "0x" + "<ここにStorage.binの中身を入れます>" 165 | compiled = "0x" + "608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea26469706673582212206713dacf42c74711b2f76573c9cdded1022c2aec004e46a23af6f351978fca0964736f6c634300080a0033" 166 | 167 | 168 | # コントラクトをデプロイします 169 | storage = storage.new({from: eth.accounts[0], data: compiled, gas: 1000000}, function(err, contract) { 170 | if (err) { console.log(err); return; } 171 | if(!contract.address) { console.log("transaction send: transactionHash: " + contract.transactionHash); return; } 172 | console.log("contract mined! address: " + contract.address); 173 | }) 174 | # --- output --- 175 | transaction send: transactionHash: 0x171f42fa371ca2bdf23821aa7e06e2c4841d97f90d19762cd3aa967534d37292 # デプロイトランザクションのハッシュ 176 | { 177 | abi: [{ 178 | inputs: [], 179 | name: "retrieve", 180 | outputs: [{...}], 181 | stateMutability: "view", 182 | type: "function" 183 | }, { 184 | inputs: [{...}], 185 | name: "store", 186 | outputs: [], 187 | stateMutability: "nonpayable", 188 | type: "function" 189 | }], 190 | address: undefined, 191 | transactionHash: "0x171f42fa371ca2bdf23821aa7e06e2c4841d97f90d19762cd3aa967534d37292" 192 | } 193 | > contract mined! address: 0x972edc90a4ed0d9f841979cb6bdf9a7eb26bbbb4 #デプロイされたコントラクトのアドレス 194 | ``` 195 | 196 | 197 | ## 5. コントラクトの実行 198 | デプロイしたコントラクトを実行します 199 | ```sh 200 | # "storage"という変数にコントラクトの情報が入っていることを確認します 201 | > storage 202 | # --- output --- 203 | { 204 | abi: [{ 205 | inputs: [], 206 | name: "retrieve", 207 | outputs: [{...}], 208 | stateMutability: "view", 209 | type: "function" 210 | }, { 211 | inputs: [{...}], 212 | name: "store", 213 | outputs: [], 214 | stateMutability: "nonpayable", 215 | type: "function" 216 | }], 217 | address: "0x972edc90a4ed0d9f841979cb6bdf9a7eb26bbbb4", 218 | transactionHash: "0x171f42fa371ca2bdf23821aa7e06e2c4841d97f90d19762cd3aa967534d37292", 219 | allEvents: function bound(), 220 | retrieve: function bound(), 221 | store: function bound() 222 | } 223 | 224 | # もし入っていない場合、作り直します 225 | # storage = eth.contract().at(<コントラクトのアドレス>); 226 | 227 | # '123'という数値を格納してみます 228 | storage.store(123, {from: eth.accounts[0]}, function(err, result) { 229 | if (err) { console.log(err); return; } 230 | console.log("transaction hash: ", result); 231 | }); 232 | # --- output --- 233 | transaction hash: 0x537c0a1d644ea357ad15e851f7a1207260d40b59391a7666ec79420b4d48a307 234 | 235 | # '123'が格納されたことを確認します。 236 | > storage.retrieve.call() 237 | 123 238 | ``` 239 | -------------------------------------------------------------------------------- /part1/Storage.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"name":"retrieve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"num","type":"uint256"}],"name":"store","outputs":[],"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /part1/Storage.bin: -------------------------------------------------------------------------------- 1 | 608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea26469706673582212206713dacf42c74711b2f76573c9cdded1022c2aec004e46a23af6f351978fca0964736f6c634300080a0033 -------------------------------------------------------------------------------- /part1/Storage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | /** 6 | * @title Storage 7 | * @dev Store & retrieve value in a variable 8 | * @custom:dev-run-script ./scripts/deploy_with_ethers.ts 9 | */ 10 | contract Storage { 11 | 12 | uint256 number; 13 | 14 | /** 15 | * @dev Store value in variable 16 | * @param num value to store 17 | */ 18 | function store(uint256 num) public { 19 | number = num; 20 | } 21 | 22 | /** 23 | * @dev Return value 24 | * @return value of 'number' 25 | */ 26 | function retrieve() public view returns (uint256){ 27 | return number; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /part1/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 15, 4 | "homesteadBlock": 0, 5 | "eip150Block": 0, 6 | "eip155Block": 0, 7 | "eip158Block": 0, 8 | "byzantiumBlock": 0, 9 | "constantinopleBlock": 0, 10 | "petersburgBlock": 0, 11 | "istanbulBlock": 0, 12 | "berlinBlock": 0, 13 | "londonBlock": 0, 14 | "terminalTotalDifficultyPassed": true, 15 | "clique": { 16 | "period": 10, 17 | "epoch": 30000 18 | } 19 | }, 20 | "coinbase": "0x0000000000000000000000000000000000000000", 21 | "difficulty": "0x20000", 22 | "gasLimit": "0x2fefd8", 23 | "nonce": "0x0000000000000042", 24 | "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", 25 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 26 | "timestamp": "0x00", 27 | "extradata": "0x0000000000000000000000000000000000000000000000000000000000000000[miner address]0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 28 | "alloc": { 29 | "0x[miner address]": { 30 | "__name__": "Miner Account", 31 | "balance": "0x56bc75e2d63100000" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /part2/README.md: -------------------------------------------------------------------------------- 1 | # Tokenコントラクトを作ってみよう 2 | 3 | ## Steps 4 | 1. Remixを使ってみよう 5 | 2. Tokenコントラクトの仕様解説 6 | 3. Tokenコントラクトの開発 7 | 4. 動かしてみよう 8 | 9 | ## 1. Remixを使ってみよう 10 | [Remix](https://remix-project.org/)はSolidityの統合開発環境です。 11 | ブラウザ上でコントラクトの動作確認ができます。 12 | デフォルトで入っているプロジェクトを通して、操作感を把握します。 13 | part1で使った[Storage.sol](../part1/Storage.sol)は、デフォルトプロジェクトに入ってます。 14 | 15 | ## 2. Tokenコントラクトの仕様解説 16 | ERC20で規格されたTokenを簡略化した仕様のTokenを開発します。 17 | まず、TokenとはEthereum上で発行するポイントのようなものです。楽天ポイントやLineポイントのように自分達で自由に発行できます。 18 | ただ、取引所でトレードできるように、規格が統一されています。20番目に提案された規格なので`ERC20`という名前がついています。 19 | 詳しい説明は「[ERC-20Tokenの紹介](https://academy.binance.com/ja/articles/an-introduction-to-erc-20-tokens)」をご参照ください。 20 | 21 | ### 仕様 22 | 1. Tokenの名前がわかる 23 | 2. 発行できる 24 | 3. 総発行数がわかる 25 | 4. 所有数がわかる 26 | 5. Tokenを誰かに送れる 27 | 28 | こちらの5ステップが実行できる基本的な機能を実装します。 29 | 30 | #### 1. Tokenの名前がわかる 31 | Token名を返す関数 32 | ```javascript 33 | function name() public view returns (string) 34 | ``` 35 | 36 | #### 2. 発行できる 37 | 「誰(`account`)に対して、何枚(`amount`)発行する」ような関数 38 | ```javascript 39 | function mint(address account, uint256 amount) public; 40 | ``` 41 | 42 | #### 3. 総発行数がわかる 43 | Tokenの総発行量を返す 44 | ```javascript 45 | function totalSupply() public view returns (uint256) 46 | ``` 47 | 48 | #### 4. 所有数がわかる 49 | 「誰(`account`)が、何枚Tokenを持っているか」を返す関数 50 | ```javascript 51 | function balanceOf(address account) public view returns (uint256) 52 | ``` 53 | 54 | #### 5. Tokenを誰かに送れる 55 | 「自分の所有するTokenを、誰(`to`)に対して、何枚(`amount`)を送る」ような関数 56 | ```javascript 57 | function transfer(address to, uint256 amount) public 58 | ``` 59 | 60 | ## 3. Tokenコントラクトの開発 61 | 上の仕様を満たすコントラクトの例として[Token.sol](./Token.sol)を開発します。 62 | Remixでワークスペースを新設して、コードを書いてみます。コンパイルが通ることを確認します。 63 | 64 | 65 | ## 4. 動かしてみよう 66 | Remix上で動作確認します。 67 | 前回は、プライベートネット上にデプロイして動作確認をしました。こちらの方が実際の動作環境に近いですが、手間がかかることが難点です。 68 | 今回は、ブラウザ上で動作する仮想的なスマートコントラクトの実行環境(`Remix VM`)を使います。何といっても「deploy」ボタンを押すだけという手軽さが魅力です。 69 | -------------------------------------------------------------------------------- /part2/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract Token { 6 | 7 | string private _name; 8 | 9 | mapping(address => uint256) private _balances; 10 | 11 | uint256 private _totalSupply; 12 | 13 | event Mint(address indexed to, uint256 value); 14 | event Transfer(address indexed from, address indexed to, uint256 value); 15 | 16 | constructor(string memory name_) { 17 | _name = name_; 18 | } 19 | 20 | // 1. トークンの名前がわかる 21 | function name() public view returns (string memory) { 22 | return _name; 23 | } 24 | 25 | // 2. 発行できる 26 | function mint(address account, uint256 amount) public { 27 | require(account != address(0), "mint to the zero address"); 28 | require(amount != 0, "mint zero amount of token"); 29 | 30 | _totalSupply += amount; 31 | _balances[account] += amount; 32 | 33 | emit Mint(account, amount); 34 | } 35 | 36 | // 3. 総発行数がわかる 37 | function totalSupply() public view virtual returns (uint256) { 38 | return _totalSupply; 39 | } 40 | 41 | // 4. 所有数がわかる 42 | function balanceOf(address account) public view returns (uint256) { 43 | return _balances[account]; 44 | } 45 | 46 | // 5. トークンを誰かに送れる 47 | function transfer(address to, uint256 amount) public { 48 | require(to != address(0), "transfer to the zero address"); 49 | require(amount != 0, "transfer zero amount of token"); 50 | 51 | 52 | _balances[msg.sender] -= amount; 53 | _balances[to] += amount; 54 | 55 | emit Transfer(msg.sender, to, amount); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /part3/README.md: -------------------------------------------------------------------------------- 1 | # Tokenを配布してみよう 2 | 3 | ## Steps 4 | 1. Metamaskを使ってみよう 5 | 2. テストネットのETHを取得 6 | 3. Tokenをテストネットにデプロイしてみる 7 | 4. Tokenを発行してみる 8 | 9 | ## 1. Metamaskを使ってみよう 10 | [Metamask](https://metamask.io/)はEthereumのブラウザウォレットです。ブラグインの形でインストールされます。 11 | Chrome、Firefox、Edgeと主要なブラウザに対応していて、最も使われているウォレットです。 12 | 使いやすい反面、手数料として`0.3%`から`0.875%`を徴収されます。 13 | 14 | ## 2. テストネットのETHを取得 15 | Ethereumのテストネットはいくつか存在します。The Merge後に多くが非推奨になります。 16 | [NETWORKS](https://ethereum.org/en/developers/docs/networks/) 17 | 18 | - Sepolia 19 | - proof-of-workなテストネット。2022年夏頃にproof-of-stakeへ移行予定。長期間にわたってメンテナンスされてゆくかは未定 20 | - Goerli 21 | - proof-of-authorityなテストネット。2022年夏頃にテストネットとしては最後にproof-of-stakeに移行予定。長期間にわたってメンテナンスされてゆく予定 22 | - Ropsten (非推奨) 23 | - proof-of-workなテストネットでしたが、2022年5月にproof-of-stakeに移行した。更新はされないため非推奨 24 | - Rinkeby (非推奨) 25 | - proof-of-authorityなテストネット。古いバージョンのGethで運用されており、更新はされないため非推奨 26 | - Kovan (非推奨) 27 | - とても古いproof-of-authorityなテストネット。更新はされないため非推奨。 28 | 29 | [GoerliのFaucet(POW)](https://goerli-faucet.pk910.de/)からテスト用のETHを取得します。 30 | [POS移行後](https://goerli-faucet.mudit.blog/) 31 | 32 | ## 3. Tokenをテストネットにデプロイしてみる 33 | 前回開発した[Token.sol](../part2/Token.sol)をGoerliにデプロイします。 34 | Remixを立ち上げてMetamaskと接続します。 35 | 36 | ## 4. Tokenを発行してみる 37 | mint関数を実行してTokenを自分に配布します。配布したtokenをMetamaskに取り込んでみます。 38 | -------------------------------------------------------------------------------- /part4/README.md: -------------------------------------------------------------------------------- 1 | # ロイヤリティスタンダードに対応したNFTマーケットプレイスを作ってみよう 2 | 3 | ## Steps 4 | 1. ロイヤリティスタンダードとは 5 | 2. EIP-2981の仕様解説 6 | 3. EIP-2981準拠のNFTを実装 7 | 4. 簡単なマーケットプレイスを作る 8 | 9 | ## 1. ロイヤリティスタンダードとは 10 | 一言でいうと、NFTが売買された時にクリエイターに支払われるロイヤリティ(報酬)の額を規定した規格です。 11 | [EIP-2981](https://eips.ethereum.org/EIPS/eip-2981)で定義されています。 12 | これはNFTのクリエイターに持続的に報酬を与える仕組みで、NFTが転売された時に売却価格の数パーセントがクリエイターに支払われます。意図としては、クリエイターに強いモチベーションを与えることで、多くのNFTを作成してもらい、市場を活性化することです。ポイントとなるのは、支払うかどうかは任意であり、強制されないということです。規格化により、NFTのマーケットプレイス間の互換性が担保され、クリエイターが報酬を受け取り損ねるリスクを低減します。 13 | 14 | ## 2. EIP-2981の仕様解説 15 | - TokenのIDと売却価格を入力に報酬を計算して、報酬の支払い額と支払い先アドレスを返却する 16 | - `royaltyAmount = (salePrice * feeRate) / 10000` 17 | ```solidity 18 | interface IERC2981 is IERC165 { 19 | /// ERC165 bytes to add to interface array - set in parent contract 20 | /// implementing this standard 21 | /// 22 | /// bytes4(keccak256("royaltyInfo(uint256,uint256)")) == 0x2a55205a 23 | /// bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a; 24 | /// _registerInterface(_INTERFACE_ID_ERC2981); 25 | 26 | /// @notice Called with the sale price to determine how much royalty 27 | // is owed and to whom. 28 | /// @param _tokenId - the NFT asset queried for royalty information 29 | /// @param _salePrice - the sale price of the NFT asset specified by _tokenId 30 | /// @return receiver - address of who should be sent the royalty payment 31 | /// @return royaltyAmount - the royalty payment amount for _salePrice 32 | function royaltyInfo( 33 | uint256 _tokenId, 34 | uint256 _salePrice 35 | ) external view returns ( 36 | address receiver, 37 | uint256 royaltyAmount 38 | ); 39 | } 40 | ``` 41 | - EIP-2981のインターフェースをサポートする 42 | - これはマーケットプレイスに対して、NFTがEIP-2981に準拠していることを知らせるためのもの 43 | - [EIP-165](https://eips.ethereum.org/EIPS/eip-165)で規格化されている 44 | - 自分のコントラクトが何のインターフェースをサポートしているか、別のコントラクトに知らせる仕組みを提供 45 | - `interfaceID`はすべての関数のfunction selectorのXOR 46 | - function selectorは関数のI/Oをkeccak256でハッシュ化したものの最初の4バイト 47 | - `bytes4(keccak256("royaltyInfo(uint256,uint256)")) == 0x2a55205a` 48 | - `interfaceID`を引数に`supportsInterface`を呼び出した時に`true`ならinterfaceIDに対応するinterfaceをサポートしているとみなす 49 | - EIP-2981のinterfaceIDは`0x2a55205a` 50 | ```solidity 51 | interface IERC165 { 52 | /// @notice Query if a contract implements an interface 53 | /// @param interfaceID The interface identifier, as specified in ERC-165 54 | /// @dev Interface identification is specified in ERC-165. This function 55 | /// uses less than 30,000 gas. 56 | /// @return `true` if the contract implements `interfaceID` and 57 | /// `interfaceID` is not 0xffffffff, `false` otherwise 58 | function supportsInterface(bytes4 interfaceID) external view returns (bool); 59 | } 60 | ``` 61 | 62 | ## 3. EIP-2981準拠のNFTを実装 63 | まず、EIP-2981に準拠した[RoyaltyStandard.sol](./RoyaltyStandard.sol)を実装します。 64 | そして、RoyaltyStandardを継承したNFTとして[SimpleNFT.sol](./SimpleNFT.sol)を実装します。 65 | 66 | 67 | ## 4. 簡単なマーケットプレイスを作る 68 | ロイヤリティーの分配を試すために簡単なマーケットプレイスを作ります。 69 | 仕様としては「指定の価格でNFTを出品して落札できる」とします。 70 | 71 | ## 5. コントラクトを動かしてみる 72 | 実際にNFTを発行してマーケットプレースで売買します。 73 | 74 | ### 手順 75 | 1. NFTとマーケットプレイスをデプロイ 76 | - マーケットプレイスコントラクトの引数にNFTのアドレスを指定 77 | 2. NFTを発行 78 | - SimpleNFT@mint 79 | 3. NFTコントラクトでマーケットプレースのアドレスをapprove 80 | - SimpleNFT@approve 81 | 4. マーケットプレースに出品 82 | - SimpleMarketplace@sell 83 | 5. マーケットプレースで購入 84 | - SimpleMarketplace@buy 85 | -------------------------------------------------------------------------------- /part4/RoyaltyStandard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/interfaces/IERC2981.sol"; 6 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/utils/introspection/ERC165.sol"; 7 | 8 | abstract contract RoyaltyStandard is ERC165, IERC2981 { 9 | mapping(uint256 => RoyaltyInfo) public royalties; 10 | 11 | // "10000"を100%とする 12 | uint16 public constant INVERSE_BASIS_POINT = 10000; 13 | 14 | // ロイヤリティ情報として、受け取りアドレスと利率を指定 15 | struct RoyaltyInfo { 16 | address recipient; 17 | uint16 feeRate; 18 | } 19 | 20 | function supportsInterface(bytes4 interfaceId) 21 | public 22 | view 23 | virtual 24 | override(ERC165, IERC165) 25 | returns (bool) 26 | { 27 | return 28 | interfaceId == type(IERC2981).interfaceId || // ERC-2981のインターフェースをサポート 29 | super.supportsInterface(interfaceId); 30 | } 31 | 32 | function _setTokenRoyalty( 33 | uint256 tokenId, 34 | address recipient, 35 | uint256 value 36 | ) internal { 37 | royalties[tokenId] = RoyaltyInfo(recipient, uint16(value)); 38 | } 39 | 40 | function royaltyInfo(uint256 tokenId, uint256 salePrice) 41 | external 42 | view 43 | override 44 | returns (address receiver, uint256 royaltyAmount) 45 | { 46 | RoyaltyInfo memory royalty = royalties[tokenId]; 47 | receiver = royalty.recipient; 48 | // 売却価格からロイヤリティを計算する 49 | royaltyAmount = (salePrice * royalty.feeRate) / INVERSE_BASIS_POINT; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /part4/SimpleMarketplace.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC721/IERC721.sol"; 6 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/interfaces/IERC2981.sol"; 7 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/utils/introspection/IERC165.sol"; 8 | 9 | contract SimpleMarketplace { 10 | 11 | address public immutable nft; 12 | 13 | struct Sale { 14 | address seller; 15 | uint256 price; 16 | } 17 | 18 | mapping(uint256 => Sale) public sales; 19 | 20 | 21 | event Selling(uint256 indexed tokenId, address seller, uint256 price); 22 | event Sold(uint256 indexed tokenId, address buyer); 23 | 24 | constructor(address _nft) { 25 | nft = _nft; 26 | } 27 | 28 | function sell(uint256 tokenId, uint256 price) public { 29 | require(0 < price, "price is zero"); 30 | 31 | // NFTをマーケットプレイスコントラクトの移す 32 | IERC721(nft).transferFrom(msg.sender, address(this), tokenId); 33 | // セール情報を記録 34 | sales[tokenId].seller = msg.sender; 35 | sales[tokenId].price = price; 36 | 37 | emit Selling(tokenId, msg.sender, price); 38 | } 39 | 40 | 41 | function buy(uint256 tokenId) public payable { 42 | uint256 price = sales[tokenId].price; 43 | address seller = sales[tokenId].seller; 44 | address buyer = msg.sender; 45 | require(price != 0, "nft on sele not found"); 46 | require(msg.value == price, "sent eth dosen't match with price"); 47 | 48 | // セール情報の初期化 49 | sales[tokenId].price = 0; 50 | sales[tokenId].seller = address(0); 51 | 52 | uint256 payment = price; 53 | 54 | // ERC-2981をサポートしているかチェック 55 | if (IERC165(nft).supportsInterface(type(IERC2981).interfaceId)) { 56 | (address receiver, uint256 royaltyAmount) = IERC2981(nft).royaltyInfo(tokenId, price); 57 | // サポートしていれば、ロイヤリティを支払う 58 | payment -= royaltyAmount; 59 | payable(receiver).transfer(royaltyAmount); 60 | } 61 | 62 | // 代金をsellerに支払う 63 | payable(seller).transfer(payment); 64 | // NFTをbuyerに移転する 65 | IERC721(nft).safeTransferFrom(address(this), buyer, tokenId); 66 | 67 | emit Sold(tokenId, buyer); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /part4/SimpleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | import "github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC721/ERC721.sol"; 6 | import "./RoyaltyStandard.sol"; 7 | 8 | contract SimpleNFT is ERC721, RoyaltyStandard { 9 | 10 | // ロイヤリティは売買価格の3%とする 11 | uint256 public constant feeRate = 300; 12 | 13 | address public immutable royaltyRecipient; 14 | 15 | constructor() ERC721("SimpleNFT", "SFT") { 16 | royaltyRecipient = msg.sender; 17 | } 18 | 19 | function mint(address to, uint256 tokenId) public { 20 | _mint(to, tokenId); 21 | // ロイヤリティ情報を設定 22 | _setTokenRoyalty(tokenId, royaltyRecipient, feeRate); 23 | } 24 | 25 | function supportsInterface(bytes4 interfaceId) 26 | public 27 | view 28 | virtual 29 | override(ERC721, RoyaltyStandard) // RoyaltyStandardをサポートする 30 | returns (bool) 31 | { 32 | return super.supportsInterface(interfaceId); 33 | } 34 | } -------------------------------------------------------------------------------- /part5/MemberList.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract MemberList { 6 | address public immutable trustedRelayer; 7 | 8 | 9 | constructor(address relayer) { 10 | trustedRelayer = relayer; 11 | } 12 | 13 | struct Member { 14 | string name; 15 | uint8 age; 16 | bool isMale; 17 | } 18 | 19 | mapping(address => Member) public list; 20 | 21 | function regist(string memory name, uint8 age, bool isMale) public { 22 | address account = originalSender(); 23 | list[account].name = name; 24 | list[account].age = age; 25 | list[account].isMale = isMale; 26 | } 27 | 28 | function originalSender() internal view returns (address sender) { 29 | if (msg.sender == trustedRelayer) { 30 | // The assembly code is more direct than the Solidity version using `abi.decode`. 31 | assembly { 32 | sender := shr(96, calldataload(sub(calldatasize(), 20))) 33 | } 34 | } else { 35 | return msg.sender; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part5/README.md: -------------------------------------------------------------------------------- 1 | # ガス代を肩代わりするmeta transactionのRelayerを作ってみる 2 | 3 | ## Steps 4 | 1. meta transactionとは 5 | 2. calldataの作り方 6 | 3. meta transactionの実行の仕方 7 | 4. 簡単な名簿コントラクトを作り、meta transactionで実行してみる 8 | 9 | ## 1. meta transactionとは 10 | 一言でいうと「実際に実行したい処理のトランザクション」をラップしたトランザクションです。ラップすることで`実際に実行したい処理`のガス代を肩代わりすることが可能です。`実際に実行したい処理`は、その処理を実行させたいユーザによって署名されていて、そのユーザのコンテキストで実行されます。 11 | 12 | 例えば、とあるDappsの運営者が、Ethereumウォレットの扱いに不慣れであったり、ETHを所有していなユーザも取り込みたい場合に使われます。ガス代を支払うのDapps運営者で、Dapps利用者はガス代支払うことなく、サービスを利用できます。 13 | 14 | ### コントラクトの構成 15 | こちらのブログ([Ethereum Meta-Transactions 101](https://medium.com/coinmonks/ethereum-meta-transactions-101-de7f91884a06))から借用させていただきました。 16 | ContractAを実行するために、Relayerコントラクトを経由します。Singerはcalldataとその署名をSenderに渡し、Senderが実際のトランザクションを発行します。 17 | ![architecture.png](./img/architecture.png) 18 | 19 | ## 2. calldataの作り方 20 | meta transactionのcalldataの作り方を具体的なjavascriptのコードを使って解説します。 21 | ライブラリとしては最もポピュラーな[web3.js](https://web3js.readthedocs.io/en/v1.8.0/)を使います。 22 | 23 | 1. まず、実行したい関数のコールデータを作ります。後述する名簿コントラクトに新しく`tom`というメンバーを追加するためのものです。 24 | ```javascript 25 | // 関数のcalldataを作る 26 | const name = "tom"; 27 | const age = 21; 28 | const isMale = true; 29 | const abiEncodedCall = web3.eth.abi.encodeFunctionCall({ 30 | name: 'regist', 31 | type: 'function', 32 | inputs: [ 33 | {type: 'string', name: 'name' }, 34 | {type: 'uint8', name: 'age' }, 35 | {type: 'bool', name: 'isMale' }, 36 | ] 37 | }, [name, age, isMale]); 38 | ``` 39 | 40 | 2. コールデータのハッシュ値を計算します。コールデータ自体に署名するのではなくハッシュ値に署名するのが慣習です。 41 | ```javascript 42 | const hash = web3.utils.soliditySha3(abiEncodedCall); 43 | ``` 44 | 45 | 3. 秘密鍵でハッシュ値に署名します。実はこの時、ハッシュ値自体に署名しているのではなく、ハッシュ値をラップしたものに署名しています。具体的には`"\x19Ethereum Signed Message:\n" + ハッシュ値.length + ハッシュ値`に対して署名しています。このラップはパスワードをハッシュ化するときのsaltと同様の役目があります。[web3.eth.accounts.sign](https://web3js.readthedocs.io/en/v1.8.0/web3-eth-accounts.html#sign)で署名すると、自動的にラップしてくれます。 46 | ```javascript 47 | // 秘密鍵に対応するアドレス: 0xE3b0DE0E4CA5D3CB29A9341534226C4D31C9838f 48 | const PRI_KEY = "d1c71e71b06e248c8dbe94d49ef6d6b0d64f5d71b1e33a0f39e14dadb070304a" 49 | const wallet = web3.eth.accounts.privateKeyToAccount(PRI_KEY); 50 | const sig = await web3.eth.accounts.sign(hash, wallet.privateKey); 51 | ``` 52 | 53 | ## 3. meta transactionの実行の仕方 54 | 実際のsolidityのコードを参照しながら、[Relayer](./Relayer.sol)コントラクトでmeta transactionの実行の仕方を解説します。 55 | 56 | 1. まずは、署名を検証します。calldataのハッシュ値を作り、それをラップするところは、署名を作った時と同じです。Solidityでは、署名からSingerを復元することができます。それを行うのが`ecrecover`です。復元されたアドレスが、オリジナルのSender(ガス代を肩代わりしたいユーザ)のアドレスと一致することを確認します。 57 | ```solidity 58 | // calldataのハッシュ値を計算 59 | bytes32 hash = keccak256(data); 60 | // hash値をメッセージでラップ 61 | bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); 62 | // ecrecoverでsingerを復元 63 | address signer = ecrecover(ethSignedMessageHash, v, r, s); 64 | // signerがオリジナルの実行者と同じであることを確認 65 | bool ok = signer == sender; 66 | ``` 67 | 68 | 2. calldataの末尾にオリジナルのSenderのアドレスを付与します。これは、callされる外部コントラクト側でオリジナルのSenderを復元できるようにするために行います。 69 | ```solidity 70 | bytes memory cdata = abi.encodePacked(data, sender); 71 | ``` 72 | 73 | 3. 外部コントラクトを実行します。`call`関数を呼び出すことで、外部コントラクトの関数が実行されます。 74 | ```solidity 75 | (bool success, bytes memory returndata) = to.call(cdata); 76 | ``` 77 | 78 | ## 4. 簡単な名簿コントラクトを作り、meta transactionで実行してみる 79 | 最後に、簡単なコントラクトを作って、meta transactionを試してみます。 80 | 作成するコントラクトはメンバーのリストを持つ名簿コントラクト([MemberList](./MemberList.sol))です。 81 | 名簿コントラクトの`regist`関数を実行します。 82 | 83 | ### Steps 84 | 1. Relayerコントラクトをデプロイ 85 | 2. 名簿コントラクトをデプロイ 86 | - デプロイ時にRelayerコントラクトのアドレスを指定します。Relayerから呼び出された場合に、`msg.sender`をcalldataの末尾から復元します。 87 | 3. calldataと署名を作る 88 | - `node sign.js`を実行します。コマンドラインにcalldataと署名が出力されます。 89 | 4. Relayコントラクトのexecute関数を実行します 90 | - 正しい署名を提出すると、名簿コントラクトが呼び出されます。呼び出し結果が正常であれば、そのまま終了します。異常があればrevertします。 91 | 5. 名簿コントラクトに名簿が追加されたか確認します 92 | - list関数を呼び出して`tom`が登録されているか確認します。 93 | -------------------------------------------------------------------------------- /part5/Relayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract Relayer { 6 | 7 | function execute( 8 | address to, 9 | address sender, 10 | bytes calldata data, 11 | uint8 v, 12 | bytes32 r, 13 | bytes32 s 14 | ) public { 15 | // 署名の検証 16 | require( 17 | verify(sender, data, v, r, s), 18 | "signature does not match request" 19 | ); 20 | 21 | // calldataの末尾にオリジナルの関数実行者のアドレスを付与 22 | bytes memory cdata = abi.encodePacked(data, sender); 23 | 24 | // 外部関数のコール 25 | (bool success, bytes memory returndata) = to.call(cdata); 26 | 27 | // 失敗した場合はrevert 28 | if (!success) { 29 | if (returndata.length > 0) { 30 | assembly { 31 | let returndata_size := mload(returndata) 32 | revert(add(32, returndata), returndata_size) 33 | } 34 | } 35 | revert("call reverted without message"); 36 | } 37 | } 38 | 39 | function verify( 40 | address sender, 41 | bytes calldata data, 42 | uint8 v, 43 | bytes32 r, 44 | bytes32 s 45 | ) internal pure returns (bool) { 46 | // calldataのハッシュ値を計算 47 | bytes32 hash = keccak256(data); 48 | // hash値をメッセージでラップ 49 | bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); 50 | // ecrecoverでsingerを復元 51 | address signer = ecrecover(ethSignedMessageHash, v, r, s); 52 | // signerがオリジナルの実行者と同じであることを確認 53 | return signer == sender; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /part5/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openreachtech/solidity-training/bb95d201d26eb0726d691e4ec21c3757beb094a3/part5/img/architecture.png -------------------------------------------------------------------------------- /part5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "ガス代を肩代わりするmeta transactionのRelayerを作ってみる", 3 | "scripts": { 4 | "sign": "node sign.js" 5 | }, 6 | "author": "tak", 7 | "license": "Apache License 2.0", 8 | "dependencies": { 9 | "web3": "^1.8.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /part5/sign.js: -------------------------------------------------------------------------------- 1 | const Web3 = require("web3"); 2 | 3 | // 秘密鍵に対応するアドレス: 0xE3b0DE0E4CA5D3CB29A9341534226C4D31C9838f 4 | const PRI_KEY = "d1c71e71b06e248c8dbe94d49ef6d6b0d64f5d71b1e33a0f39e14dadb070304a" 5 | 6 | async function main() { 7 | const web3 = new Web3(); 8 | const wallet = web3.eth.accounts.privateKeyToAccount(PRI_KEY); 9 | 10 | // 関数のcalldataを作る 11 | const name = "tom"; 12 | const age = 21; 13 | const isMale = true; 14 | const abiEncodedCall = web3.eth.abi.encodeFunctionCall({ 15 | name: 'regist', 16 | type: 'function', 17 | inputs: [ 18 | {type: 'string', name: 'name' }, 19 | {type: 'uint8', name: 'age' }, 20 | {type: 'bool', name: 'isMale' }, 21 | ] 22 | }, [name, age, isMale]); 23 | console.log(`calldate: ${abiEncodedCall}`) 24 | 25 | // calldataのハッシュ値に署名する 26 | const hash = web3.utils.soliditySha3(abiEncodedCall); 27 | const sig = await web3.eth.accounts.sign(hash, wallet.privateKey); 28 | console.log(sig) 29 | } 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /part6/BankV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract BankV1 { 6 | 7 | string public name; 8 | 9 | uint256 public totalBalance; 10 | 11 | mapping(address => BankAccount) public bankAccounts; 12 | 13 | struct BankAccount { 14 | uint256 balance; 15 | } 16 | 17 | function setName(string memory newName) public { 18 | name = newName; 19 | } 20 | 21 | function deposit() public payable { 22 | totalBalance += msg.value; 23 | bankAccounts[msg.sender].balance = msg.value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /part6/BankV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache License 2.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract BankV2 { 6 | 7 | string public name; 8 | 9 | uint256 public totalBalance; 10 | 11 | mapping(address => BankAccount) public bankAccounts; 12 | 13 | struct BankAccount { 14 | uint256 balance; 15 | } 16 | 17 | function setName(string memory newName) public { 18 | name = newName; 19 | } 20 | 21 | function deposit() public payable { 22 | totalBalance += msg.value; 23 | bankAccounts[msg.sender].balance = msg.value; 24 | } 25 | 26 | function withdraw(uint256 amount) public { 27 | require(amount =0.7.0 <0.9.0; 4 | 5 | abstract contract Proxy { 6 | 7 | function _delegate(address _implementation) internal returns (bytes memory) { 8 | assembly { 9 | // calldataをメモリにコピーする 10 | calldatacopy(0, 0, calldatasize()) 11 | 12 | // delegate callの実行 13 | // delegatecall(消費可能なガス残量, 呼び出し先, メモリオフセット, メモリサイズ, 実行結果オフセット、実行結果サイズ) 14 | // 実行結果のサイズは不明なのでゼロを指定 15 | let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) 16 | 17 | // 実行結果をメモリにコピー 18 | returndatacopy(0, 0, returndatasize()) 19 | 20 | switch result 21 | // 戻り値が“0”の場合は失敗なのでrevert 22 | case 0 { 23 | revert(0, returndatasize()) 24 | } 25 | // 戻り値が“1”の場合は成功なので、結果を返却 26 | default { 27 | return(0, returndatasize()) 28 | } 29 | } 30 | } 31 | 32 | function implementation() public view virtual returns (address); 33 | 34 | // 存在しない関数が呼ばれたときに実行される 35 | // 👉 delegatecallで呼び出す先のコントラクトの関数はProxyで実装していない。したがって、fallbackが呼ばれる 36 | fallback() external payable virtual { 37 | _delegate(implementation()); 38 | } 39 | 40 | // calldataなしでethが送られたときに実行される 41 | // 👉 delegatecallで呼び出す先のコントラクトのreceive ethを実行する 42 | receive() external payable virtual { 43 | _delegate(implementation()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /part6/README.md: -------------------------------------------------------------------------------- 1 | # Upgrade可能なコントラクトを作ってみる 2 | 3 | ## Steps 4 | 1. どうやってアップグレードするのか? 5 | 2. 具体的にどのような仕組みなのか? 6 | 3. 実装方法の解説 7 | 4. Bankコントラクトを作ってアップグレードしてみる 8 | 9 | ## 1. どうやってアップグレードするのか? 10 | ロジックの部分を別のコントラクトに切り出しておき、ロジックコントラクトを切り替えることUpgradeを可能とします。スマートコントラクトはひとたびデプロイされたら変更することができません。しがたって、ロジックの部分を直接変更することができません。そこで、全く新しいコントラクトを新たに作り、そちらに切り替えることで、アップグレードを可能とします。 11 | 12 | 1点、注意すべき制約があります。それは`データ構造の変更は効かない`ということです。 13 | ロジック部分を切り替える一方、データが格納されているストレージ部分は共通です。旧バージョンのストレージがそのまま新バージョンに引き継がれます。 14 | 15 | ## 2. 具体的にどのような仕組みなのか? 16 | こちら([Solidity: Upgradable Contracts, Tokens](https://www.blocksism.com/solidity-upgradable-token-contract/))のブロブの画像をお借りして具体的な仕組みを解説します。 17 | 18 | とあるカウンターコントラクトをアップグレード可能な形で実装する場合を考えます。 19 | カウンターコントラクトは`value`というカウンターを持ち、`incrementValue`でインクリメントされ、`decrementValue`でデクリメントされます。 20 | 21 | カウンターコントラクト(`implementationContract`)をアップグレード可能な形にするには、Proxyコントラクトを間に噛ませます。Proxyコントラクトがカウンターコントラクトのロジックを実行し、結果を自身のストレージに格納します。 22 | - Proxyコントラクトのfallback関数を経由して、カウンターコントラクトの2つの関数が呼び出されます 23 | - fallback関数は「存在しない関数が呼ばれたときに実行される」ものです 24 | - Proxyコントラクトには存在しない`incrementValue`や`decrementValue`を呼び出そうとしたときに、結果的に、このfallback関数をが呼ばれます 25 | - fallback関数内でカウンターコントラクトの関数を呼び出します 26 | - 関数を実行した結果、変更されたカウンター(value)の新しい値を、Proxyコントラクト自体に格納します 27 | - Proxyコントラクトはデータを格納するストレージの役割を果たします 28 | - Proxyコントラクトはvalue等、呼び出し先コントラクトのデータ以外にも`どのコントラクトを呼び出すか(_implAddr)`を保持します 29 | 30 | ![version 1](./img/v1.png) 31 | 32 | 呼び出し先コントラクトのアドレス(_implAddr)を新しいカウンターコントラクト(`implementationContractV2`)に切り替えます。Proxyコントラクト自体は変更されていないので、ストレージは保持されたままで、ロジック部分だけ新しいコントラクトに切り替わります・ 33 | 34 | ![version 2](./img/v2.png) 35 | 36 | 37 | 38 | ## 3. 実装方法の解説 39 | 肝になるは`delegatecall`という関数です。これは、呼び出し先のコントラクトをあたかも自分のコントラクトであるかのように呼び出す関数です。通常、外部コントラクトを呼び出し何かしら変更を加えた場合、変更は外部コントラクトに適応されます。しかし、delegatecallの場合は、自分のコントラクトに適応されます。 40 | 41 | 実装例としては、このような形になります。`delegatecall`の部分に注目すると、メモリにコピーしたcalldataをもとに、`_implementation`という外部コントラクトを呼び出します。 42 | ```solidity 43 | assembly { 44 | // calldataをメモリにコピーする 45 | calldatacopy(0, 0, calldatasize()) 46 | 47 | // delegate callの実行 48 | // delegatecall(消費可能なガス残量, 呼び出し先, メモリオフセット, メモリサイズ, 実行結果オフセット、実行結果サイズ) 49 | // 実行結果のサイズは不明なのでゼロを指定 50 | let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) 51 | 52 | // 実行結果をメモリにコピー 53 | returndatacopy(0, 0, returndatasize()) 54 | 55 | switch result 56 | // 戻り値が“0”の場合は失敗なのでrevert 57 | case 0 { 58 | revert(0, returndatasize()) 59 | } 60 | // 戻り値が“1”の場合は成功なので、結果を返却 61 | default { 62 | return(0, returndatasize()) 63 | } 64 | } 65 | ``` 66 | 67 | delegatecallを使う上で注意すべき点は「ストレージ変数のコンフリクト」です。 68 | ストレージ変数とは、ストレージに格納される変数です。ローカル変数と異なり、永続化されます。 69 | 例えば、以下のコントラクトの`account`や`age`や`isMale`です。 70 | Ethereumでは、このストレージ変数が順番に2^256個のスロットに割り当てられていきます。 71 | `account -> slot1`, `age -> slot2`, `isMale -> slot3`といった形です。 72 | ```solidity 73 | contract OuterContract { 74 | address public account; // <- slot1 75 | uint8 public age; // <- slot2 76 | bool public isMale; // <- slot3 77 | } 78 | ``` 79 | 80 | 下の`UpgradableContract`の外部関数として上の`OuterContract`をdelegatecallすると「ストレージ変数のコンフリクト」が発生します。UpgradableContractの`owner`とOuterContractの`account`がコンフリクトします。 81 | delegatecallによる変更を、呼び出し先のコントラクトに適応される際に、ストレージのレイアウトは引き継がれます。 82 | したがって、UpgradableContractを実装するときは、OuterContractのストレージレイアウトとコンフリクトしないように注意する必要があります。 83 | ```solidity 84 | contract UpgradableContract { 85 | address public owner; // <- slot1 86 | } 87 | ``` 88 | 89 | ちなみに、配列やmapのslotはとある規則に従ったハッシュ値から算出されます。[Layout of State Variables in Storage](https://docs.soliditylang.org/en/v0.8.16/internals/layout_in_storage.html#layout-of-state-variables-in-storage)にまとまっているので、興味のある方は見ておいてください。 90 | 91 | ### EIP-1967: Proxy Storage Slotsについて 92 | Upgrade可能なコントラクトを実装するには「呼び出し先のコントラクト」と「呼び出し先のコントラクトを変更できるオーナー」の最低2つのストレージ変数が必要です。これらをコンフリクトすることなく実装するための規格が[EIP-1967: Proxy Storage Slots](https://eips.ethereum.org/EIPS/eip-1967)です。 93 | 94 | 簡単に説明すると特定のSlotにそれぞれを格納するというものです。 95 | 「呼び出し先のコントラクト」を`0x360894a1...`に割り当て、「呼び出し先のコントラクトを変更できるオーナー」を`0xb5312768...`に割り当てます。 96 | これらは特定の文字列のハッシュ値として計算されます。 97 | ```solidity 98 | // "eip1967.proxy.implementation"のハッシュ値 99 | // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)) 100 | bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 101 | 102 | // "eip1967.proxy.admin"のハッシュ値 103 | // bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)) 104 | bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; 105 | ``` 106 | 107 | Solidityでは、ストレージ変数の格納先slotを指定することができます。 108 | `AddressSlot`というstructの格納先slotに上述の`0x3608...`と`0xb531...`を指定する形です。 109 | 実際のアドレスは、AddressSlotの`value`の中に入ります。 110 | ```solidity 111 | // 「呼び出し先のコントラクト」と「呼び出し先のコントラクトを変更できるオーナー」のアドレスを格納するstruct 112 | struct AddressSlot { 113 | address value; 114 | } 115 | 116 | AddressSlot storage r; 117 | 118 | // 0x3608... or 0xb531... 119 | bytes32 slot; 120 | 121 | // "AddressSlot"の変数rを格納するslotを指定する 122 | assembly { 123 | r.slot := slot 124 | } 125 | ``` 126 | `.slot`に扱いについて、詳しくは[Access to External Variables, Functions and Libraries](https://docs.soliditylang.org/en/v0.8.17/assembly.html#access-to-external-variables-functions-and-libraries)を参照ください。 127 | 128 | 129 | ## 4. Bankコントラクトを作ってアップグレードしてみる 130 | 試しにBankコントラクトを作って、アップグレードしてみます。 131 | 132 | #### version 1のBankコントラクト 133 | 134 | version 1のBankコントラクトの機能としては「バンク名を変更できる」「お金を入金できる」とします。 135 | ```solidity 136 | contract BankV1 { 137 | 138 | string public name; 139 | 140 | uint256 public totalBalance; 141 | 142 | mapping(address => BankAccount) public bankAccounts; 143 | 144 | struct BankAccount { 145 | uint256 balance; 146 | } 147 | 148 | function setName(string memory newName) public { 149 | name = newName; 150 | } 151 | 152 | function deposit() public payable { 153 | totalBalance += msg.value; 154 | bankAccounts[msg.sender].balance = msg.value; 155 | } 156 | } 157 | ``` 158 | 159 | #### version 2のBankコントラクト 160 | version2で「お金を引き出せる」機能を追加します。 161 | ```solidity 162 | contract BankV2 { 163 | ... 164 | function withdraw(uint256 amount) public { 165 | require(amount =0.7.0 <0.9.0; 4 | 5 | import "./Proxy.sol"; 6 | 7 | contract UpgradeProxy is Proxy { 8 | 9 | struct AddressSlot { 10 | address value; 11 | } 12 | 13 | // "eip1967.proxy.implementation"のハッシュ値 14 | // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)) 15 | bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 16 | 17 | // "eip1967.proxy.admin"のハッシュ値 18 | // bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)) 19 | bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; 20 | 21 | event Upgraded(address indexed implementation); 22 | event AdminChanged(address previousAdmin, address newAdmin); 23 | 24 | modifier onlyAdmin() { 25 | require(msg.sender == admin(), "caller should be admin"); 26 | _; 27 | } 28 | 29 | constructor(address _implementation) { 30 | _upgradeTo(_implementation); 31 | // deployしたユーザをadminユーザとしてセット 32 | _changeAdmin(msg.sender); 33 | } 34 | 35 | function implementation() public view override returns (address) { 36 | return getAddressSlot(_IMPLEMENTATION_SLOT).value; 37 | } 38 | 39 | // adminユーザのみ実行可能 40 | function upgradeTo(address newImplementation) public onlyAdmin { 41 | _upgradeTo(newImplementation); 42 | } 43 | 44 | function _upgradeTo(address newImplementation) internal { 45 | require(isContract(newImplementation), "new implementation is not contract"); 46 | // "_IMPLEMENTATION_SLO"を"AddressSlot"のアドレスとして初期化し、 47 | // AddressSlotの中身にロジックコントラクトのアドレスを格納 48 | getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; 49 | emit Upgraded(newImplementation); 50 | } 51 | 52 | function admin() public view returns (address) { 53 | return getAddressSlot(_ADMIN_SLOT).value; 54 | } 55 | 56 | // adminユーザのみ実行可能 57 | function changeAdmin(address newAdmin) public onlyAdmin { 58 | _changeAdmin(newAdmin); 59 | } 60 | 61 | function _changeAdmin(address newAdmin) internal { 62 | address oldAdmin = admin(); 63 | // "_ADMIN_SLOT"を"AddressSlot"のアドレスとして初期化し、 64 | // AddressSlotの中身にadminユーザのアカウントアドレスを格納 65 | getAddressSlot(_ADMIN_SLOT).value = newAdmin; 66 | emit AdminChanged(oldAdmin, newAdmin); 67 | } 68 | 69 | // 引数で渡されたslotを"AddressSlot"のアドレスとして使う 70 | function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { 71 | // "AddressSlot"の変数rを格納するslotを指定する 72 | assembly { 73 | r.slot := slot 74 | } 75 | } 76 | 77 | function isContract(address account) internal view returns (bool) { 78 | // extcodesizeがゼロの場合、コントラクトとみなす 79 | return account.code.length > 0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /part6/calldata.js: -------------------------------------------------------------------------------- 1 | const Web3 = require("web3"); 2 | 3 | async function main() { 4 | const web3 = new Web3(); 5 | 6 | // "setName"のcalldataを作る 7 | const newName = "piggy bank"; 8 | let abiEncodedCall = web3.eth.abi.encodeFunctionCall({ 9 | name: 'setName', 10 | type: 'function', 11 | inputs: [ 12 | {type: 'string', name: 'newName' },, 13 | ] 14 | }, [newName]); 15 | console.log(`calldate of setName: ${abiEncodedCall}\n`) 16 | 17 | // "deposit"のcalldataを作る 18 | abiEncodedCall = web3.eth.abi.encodeFunctionCall({ 19 | name: 'deposit', 20 | type: 'function', 21 | inputs: [] 22 | }, []); 23 | console.log(`calldata of deposit: ${abiEncodedCall}\n`) 24 | 25 | // "withdraw"のcalldataを作る 26 | const amount = "10000000000000000000" // 10 ETH 27 | abiEncodedCall = web3.eth.abi.encodeFunctionCall({ 28 | name: 'withdraw', 29 | type: 'function', 30 | inputs: [ 31 | {type: 'uint256', name: 'amount' }, 32 | ] 33 | }, [amount]); 34 | console.log(`calldata of withdraw: ${abiEncodedCall}\n`) 35 | } 36 | 37 | main() 38 | -------------------------------------------------------------------------------- /part6/img/v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openreachtech/solidity-training/bb95d201d26eb0726d691e4ec21c3757beb094a3/part6/img/v1.png -------------------------------------------------------------------------------- /part6/img/v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openreachtech/solidity-training/bb95d201d26eb0726d691e4ec21c3757beb094a3/part6/img/v2.png -------------------------------------------------------------------------------- /part6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Upgrade可能なコントラクトを作ってみる", 3 | "scripts": { 4 | "calldata": "node calldata.js" 5 | }, 6 | "author": "tak", 7 | "license": "Apache License 2.0", 8 | "dependencies": { 9 | "web3": "^1.8.0" 10 | } 11 | } 12 | --------------------------------------------------------------------------------