# Homework ## HW 1 寫出可以更新的智能合約,如:Uniswap v1 -> v2 -> v3 ## 說明 使用 OpenZeppelin Upgrades 的架構,它會自動幫我們產出以下檔案 : 1. ProxyAdmin.sol 2. TransparentUpgradeableProxy.sol (這裡簡稱 Proxy.sol) * `user` 透過 `Proxy.sol` 這個介面操作 `Box.sol` 的邏輯功能,`user` **不會感覺到 `Proxy.sol` 的存在**,因為 `Proxy.sol` 會綁定 `Box.sol` 的合約地址,並使用 delegatecall 的方式呼叫。 * 如果 `Box.sol` 的有**漏洞**或想**新增功能**,可以新增一個 `BoxV2.sol`,並透過 `ProxyAdmin.sol` 核准 `Proxy.sol` 綁定 `BoxV2.sol` 的合約地址。 ## 架構 ``` # version 1 user ---> Proxy.sol ---> Box.sol ^ ^ ^ delegatecall # version 2 user ---> Proxy.sol ---> BoxV2.sol ^ ^ update() ^ owner ---> ProxyAdmin.sol ``` ## 範例代碼,解釋 delegatecall 的實作 [delegatecall 的實作](/ItGDRnYYTCGLCV2D314l6Q) --- ## 基本安裝 ```shell= mkdir mycontract && cd mycontract npm init -y # 快速創建環境 npx hardhat npm install --save-dev @openzeppelin/hardhat-upgrades npm install --save-dev @nomiclabs/hardhat-ethers ethers npm install --save-dev @nomiclabs/hardhat-etherscan # 使用環境變數 .env npm install dotenv --save ``` --- ## 申請部屬網路 (rinkeby、ropsten) 的 URL、API key 1. [Infura](https://infura.io/) 申請 rinkeby 的 URL https://rinkeby.infura.io/v3/ca225f76e29a45... 2. [etherscan](https://etherscan.io/) 申請 API key 4ZWQ9RG6FDGCF4Z1NJFQ79D... 3. 導出錢包的私鑰 0xsadfa231... --- ## 創建 .env 檔案 .env ```shell= URL=https://rinkeby.infura.io/v3/ca22... PRIVATE_KEY=545asfsa45f... ETHERSCAN_API_KEY=4ZWQ9RG6... ``` --- ## 設定檔 hardhat.config.js ```javascript= // hardhat.config.js require("@nomiclabs/hardhat-ethers"); require("@openzeppelin/hardhat-upgrades"); require("@nomiclabs/hardhat-etherscan"); require('dotenv').config(); /** * @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.0", networks: { rinkeby: { url: process.env.URL, accounts: [process.env.PRIVATE_KEY], }, }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY, }, }; ``` --- ## 創建第一份合約 contracts/Box.sol ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Box { uint256 private value; // Emitted when the stored value changes event ValueChanged(uint256 newValue); // Stores a new value in the contract function store(uint256 newValue) public { value = newValue; emit ValueChanged(newValue); } // Reads the last stored value function retrieve() public view returns (uint256) { return value; } } ``` --- ## Box.sol 合約佈署程式 scripts/deploy.js ```javascript= async function main() { const Box = await ethers.getContractFactory("Box") console.log("Deploying Box, ProxyAdmin, and then Proxy...") const proxy = await upgrades.deployProxy(Box, [42], { initializer: 'store' }) console.log("Proxy of Box deployed to:", proxy.address) } main() .then(() => process.exit(0)) .catch(error => { console.error(error) process.exit(1) }) ``` --- ## 開始編譯和部屬到網路,這裡我們選的是 rinkeby 測試網路 ```shell= > npx hardhat run --network rinkeby scripts/deploy.js Compiling 3 files with 0.8.0 Compilation finished successfully Deploying Box, ProxyAdmin, and then Proxy... Proxy of Box deployed to: 0xF5B1dc46054427CABb941393ff4DD0264B5C214f ``` * 成功的話將會佈署 3 個合約 * Box, ProxyAdmin, Proxy(TransparentUpgradeableProxy) * Proxy 很重要,這是 User 的互動介面 * Proxy address: 0xF5B1dc46054427CABb941393ff4DD0264B5C214f # 記住它 --- ## 上傳程式碼 Box.sol 如果要到 etherscan 頁面操作 contract,完成這一步才能在 etherscan 上看到程式碼和操作 ```bash= npx hardhat verify --network rinkeby <Box.sol address> ``` --- ## 開啟 console * Proxy.sol 綁定 Box.sol 合約地址 * 如果不想要命令列操作,完成上傳程式碼,就可以到 etherscan 的頁面做點選綁定的操作 ```bash= $ npx hardhat console --network rinkeby > const Box = await ethers.getContractFactory("Box") undefined > const box = await Box.attach("0xF5B1dc46054427CABb941393ff4DD0264B5C214f") undefined > (await box.retrieve()).toString() '42' ``` --- ## 創建第二份合約 contracts/BoxV2.sol ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract BoxV2 { uint256 private value; // Emitted when the stored value changes event ValueChanged(uint256 newValue); // Stores a new value in the contract function store(uint256 newValue) public { value = newValue; emit ValueChanged(newValue); } // Reads the last stored value function retrieve() public view returns (uint256) { return value; } // Increments the stored value by 1 function increment() public { value = value + 1; emit ValueChanged(value); } } ``` ## BoxV2.sol 合約佈署程式 scripts/upgrade.js ```javascript= async function main() { // 需要輸入 proxy address const proxyAddress = '0xF5B1dc46054427CABb941393ff4DD0264B5C214f'; const BoxV2 = await ethers.getContractFactory("BoxV2") let box = await upgrades.upgradeProxy(proxyAddress, BoxV2) console.log("Your upgraded proxy is done!", box.address) } main() .then(() => process.exit(0)) .catch(error => { console.error(error) process.exit(1) }) ``` --- ## 開始編譯和部屬到網路 ```bash= > npx hardhat run --network rinkeby scripts/upgrade.js Your upgraded proxy is done! 0xF5B1dc46054427CABb941393ff4DD0264B5C214f ``` --- ## 上傳程式碼 BoxV2.sol 如果要到 etherscan 頁面操作 contract,完成這一步才能在 etherscan 上看到程式碼和操作 ```bash= npx hardhat verify --network rinkeby <BoxV2.sol address> ``` ## 打開 hardhat console * Proxy.sol 綁定 BoxV2.sol 合約地址 * 如果不想要命令列操作,完成上傳程式碼,就可以到 etherscan 的頁面做點選綁定的操作 ```bash= > npx hardhat console --network rinkeby > const BoxV2 = await ethers.getContractFactory("BoxV2") undefined > const boxV2 = await BoxV2.attach("0xF5B1dc46054427CABb941393ff4DD0264B5C214f") undefined > (await boxV2.retrieve()).toString() '42' > await boxV2.increment() { hash: ... > (await boxV2.retrieve()).toString() '43' ``` 這裡更新可能會失敗,多試幾次 --- ## 資料來源 * etherscan 頁面操作的方式 https://www.youtube.com/watch?v=JgSj7IiE4jA&list=PLO5VPQH6OWdXhkOvoptGTyQk3KI2EawUc&index=9 * 命令操作的方式 https://www.youtube.com/watch?v=bdXJmWajZRY * 沒有影片,純文字教學 https://forum.openzeppelin.com/t/openzeppelin-upgrades-step-by-step-tutorial-for-hardhat/3580 * DelegateCall: Calling Another Contract Function in Solidity https://medium.com/coinmonks/delegatecall-calling-another-contract-function-in-solidity-b579f804178c