--- tags: it 鐵人賽 30 天, Web 3, ethereum --- # 從以太坊白皮書理解 web 3 概念 - Day18 ## Learn Solidity - Day 10 - Build an Oracle 今天將會透過 [Lession 14: Build an Oracle](https://cryptozombies.io/en/lesson/14) 練習製作 Oracle Contract ### Oracle 介紹 假設今天要製作一個去中心化金融應用,這個應用讓使用者能從 Contract 把等值於美金換成 1 Ether。 為了要完成這個這個應用,應用 Contract 必須要能知道當下 1 Ether 值多少美金。 要查詢當下 Ether 對應到美金的匯率可以透過 Biance 交易所的 API 去查 然而, Contract 本身無法直接與外界 api 做互動 所以需要一個 Oracle 來當作外界資料來源。 Oracle 是 Smart Contract 用來與外界呼叫的一種機制。 其流程如下 ![](https://cryptozombies.io/course/static/image/lesson-14/EthPriceOracleOverview.png) ### 實作 1. 建立一個資料夾 mkdir EthPriceOracle ; 然後進入資料夾 2. 使用以下指令初始化專案 ```shell= npm init -y ``` or ```shell= yarn init -y ``` 3. 安裝以下套件: truffle, openzeppelin-solidity, loom-js, loom-truffle-provider, bn.js, axios ```shell= npm i truffle openzeppelin-solidity loom-js loom-truffle-provider bn.js axios ``` or ```shell= yarn add truffle openzeppelin-solidity loom-js loom-truffle-provider bn.js axios ``` 4. 建立 oracle 資料夾 ```shell= mkdir oracle ``` 在 oracle 內初始化 Truffle ```shell= cd oracle; npx truffle init; cd .. ``` 5. 建立 caller 資料夾 ```shell= mkdir caller ``` 在 caller 內初始化 Truffle ```shell= cd caller; npx truffle init; cd .. ``` 檢查資料夾結構透過 tree 指令如下 ```shell= tree -L 2 -I node_modules . ├── caller │ ├── contracts │ ├── migrations │ ├── test │ └── truffle-config.js ├── oracle │ ├── contracts │ ├── migrations │ ├── test │ └── truffle-config.js └── package.json ``` ## 呼叫其他的 Contract 在開始實作 Oracle Contract 之前 首先要來看一下呼叫 Oracle Contract 的 Caller Contract 為了要能與Oracle Contract 互動,Caller Contract 必須要俱備以下資訊 * Oracle Contract 的 address * 要呼叫 Oracle Contract function 的 function Signature 或者說介面 已知到一旦 Oracle Contract 一發佈到鏈上,要更新就只能重新發佈 為了能夠讓 Caller Contract 能夠在 Oracle Contract 更新時不被影響到要重新發佈 需要透過建立個 function 來更新 Oracle Contrat 的 address ### 實作 CallerContract 1. 宣告一個變數 address oracleAddress 並且設定為 private ```solidity= address private oracleAddress; ``` 2. 宣告一個 function setOracleInstanceAddress 需要一個參數: _oracleInstanceAddress(address) 設定存取權限為: public 3. 更新 function setOracleInstanceAddress 第一行內容如下 ```solidity= oracleAddress = _oracleInstanceAddress; ``` ```solidity= pragma solidity 0.5.0; contract CallerContract { // start here address private oracleAddress; function setOracleInstanceAddress(address _oracleInstanceAddress) public { oracleAddress = _oracleInstanceAddress; } } ``` ## 實作呼叫 Oracle Contract ### 定義呼叫介面 介面類似於 Contract,但只是用來宣告 functions signature 卻不實作。且所有 function 接需要為 external 舉例來說:假設有一個 FastFood Contract 如下 ```solidity= pragma solidity 0.5.0; contract FastFood { function makeSandwich(string calldata _fillingA, string calldata _fillingB) external { //Make the sandwich } } ``` 假設需要從另一個 PrepareLunch Contract 呼叫 makeSandSwich 就必須要定一個介面 FastFoodInterface.sol 如下 ```solidity= pragma solidity 0.5.0; interface FastFoodInterface { function makeSandwich(string calldata _fillingA, string calldata _fillingB) external; } ``` 然後在 PrepareLaunch 內引入介面 加入初始化的 FastFoodInterface 邏輯 ```solidity= fastFoodInstance = FastFoodInterface(_address); ``` 然後就可以使用 fastFoodInstance 來呼教 makeSandwich ```solidity= pragma solidity 0.5.0; import "./FastFoodInterface.sol"; contract PrepareLunch { FastFoodInterface private fastFoodInstance; function instantiateFastFoodContract (address _address) public { fastFoodInstance = FastFoodInterface(_address); fastFoodInstance.makeSandwich("sliced ham", "pickled veggies"); } } ``` ### 實作 1. 建立 caller/EthPriceOracleInterface.sol 2. 在 caller/CallerContract.sol import caller/EthPriceOracleInterface.sol 3. 宣告 EthPriceOracleInterface private oracleInstance; 4. 初始化 oracleInstance = EthPriceOracleInterface(oracleAddress); ```solidity= pragma solidity 0.5.0; //1. Import from the "./EthPriceOracleInterface.sol" file import "./EthPriceOracleInterface.sol"; contract CallerContract { // 2. Declare `EthPriceOracleInterface` EthPriceOracleInterface private oracleInstance; address private oracleAddress; function setOracleInstanceAddress (address _oracleInstanceAddress) public { oracleAddress = _oracleInstanceAddress; //3. Instantiate `EthPriceOracleInterface` oracleInstance = EthPriceOracleInterface(oracleAddress); } } ``` ## 宣告 onlyOwner modifier 在 setOracleInstanceAddress 前面 setOracleInstanceAddress 因為設定成 public 代表其他 Contract 可以任意更改 Oracle Address 因此需要使用 OpenZepelin's Ownable Contract 來限制呼叫者限定只有 owner 才能去做執行 ### 實作 1. import "openzeppelin-solidity/contracts/ownership/Ownable.sol" 2. 讓 CallerContract 繼承 Ownable 3. 在 setOracleInstanceAddress 加入 onlyOnwer 4. 建立一個 event newOracleAddressEvent 然後在 setOracleInstanceAddress 最後一行 加入 emit newOracleAddressEvent(oracleAddress); ```solidity= pragma solidity 0.5.0; import "./EthPriceOracleInterface.sol"; // 1. import the contents of "openzeppelin-solidity/contracts/ownership/Ownable.sol" import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract CallerContract is Ownable { // 2. Make the contract inherit from `Ownable` EthPriceOracleInterface private oracleInstance; address private oracleAddress; event newOracleAddressEvent(address oracleAddress); // 3. On the next line, add the `onlyOwner` modifier to the `setOracleInstanceAddress` function definition function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner { oracleAddress = _oracleInstanceAddress; oracleInstance = EthPriceOracleInterface(oracleAddress); // 4. Fire `newOracleAddressEvent` emit newOracleAddressEvent(oracleAddress); } } ``` ## 使用 mapping 來紀錄 request 接下來將講解 Oracle 更新 ETH 的價格流程 當 ETH 價格更新, Smart Contract 會呼叫 Oracle Contract s內 getLatestEthPrice 功能。 然而,因為是非同步的, getLatestEthPrice 並不會直接回傳該值。 取而代之,會回傳一個 request id 。 然後 Oracle Contract 會透過 Biance API 讀取當下的 ETH 價格,然後執行 Caller Contract 對外的 callback function Caller Contract 透過 callback function 取得最新的 ETH 價格 ### Mapping 每個 Dapp 的使用者都可以透過呼叫 Caller Contract 來發起更新 ETH 價格的 Request 。 因為 Caller Contract 無法在被呼叫時來即時處理這這些 Request 所以必須要使用一個結構來紀錄這些還沒有回應的 Request 。 這樣才能夠在之後取得當下 ETH 價格後,讓 callback 找到對應的 Request 作為回應。 可以使用一個叫作 myRequest 的 mapping 用來紀錄每個 requestID 對應的 Request 是否有回應過 ### 實作 1. 宣告一個 mapping 叫作 myRequests 如下 ```solidity= mapping(uint256 => bool) myRequests; ``` 2. 宣告一個 function updateEthPrice, 不需要任何參數,並且存取權限是 public 3. function updateEthPrice 第一行需要去呼叫 oracleInstance.getLatestEthPrice 並且把回傳值存在 uint256 id 內 4. 設定 myRequests[id] = true; 5. emit ReceivedNewRequestIdEvent(id) ```solidity= pragma solidity 0.5.0; import "./EthPriceOracleInterface.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract CallerContract is Ownable { EthPriceOracleInterface private oracleInstance; address private oracleAddress; mapping(uint256=>bool) myRequests; event newOracleAddressEvent(address oracleAddress); event ReceivedNewRequestIdEvent(uint256 id); function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner { oracleAddress = _oracleInstanceAddress; oracleInstance = EthPriceOracleInterface(oracleAddress); emit newOracleAddressEvent(oracleAddress); } // Define the `updateEthPrice` function function updateEthPrice() public { uint256 id = oracleInstance.getLatestEthPrice(); myRequests[id] = true; emit ReceivedNewRequestIdEvent(id); } } ``` ## 實作 CallBack function 呼叫 Binance API 是非同步的操作。所以 Caller Smart Contract 需要提供一個 callback function 來讓 Oracle Contract 呼叫。 以下是 callback 流程解說 1. 一開始,會需要確認傳入的 id 是正確的 所以需要使用 require 來做這件事 2. 當確認 id 是正確後 使用以下語法刪除原本在 myRequest內的值 ```solidity! delete myRequest[id]; ``` 3. 最後透過 event 更新到前端的資料 ### 實作 callback 1. 建立一個 function callback 需要兩個參數: _ethPrice(uint256), _id(uint256) 2. 在 Contract 第一行 ,宣告一個 uint256 變數 ethPrice 並且設定讀取權限為 private ```solidity= uint256 ethPrice private; ``` 3. 建立一個 event PriceUpdatedEvent 並且需要兩個參數: ethPrice(uint256), id(uint256) ```solidity= event PriceUpdatedEvent(uint256 ethPrice, uint256 id); ``` 4. 在 callback function 首先需要確認 myRequests[id] 是 true。會需要使用 require 語法如下 ```solidity= require(myRequests[id], "This request is not in my pending list."); ``` 5. 把新的 ETH price 存入 ethPrice 這個變數 6. 最後把 id 從 myRequests 移除 ```solidity= delete myRequests[id]; ``` 7. emit PriceUpdatedEvent ```solidity= pragma solidity 0.5.0; import "./EthPriceOracleInterface.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract CallerContract is Ownable { // 1. Declare ethPrice uint256 private ethPrice; EthPriceOracleInterface private oracleInstance; address private oracleAddress; mapping(uint256=>bool) myRequests; event newOracleAddressEvent(address oracleAddress); event ReceivedNewRequestIdEvent(uint256 id); // 2. Declare PriceUpdatedEvent event PriceUpdatedEvent(uint256 ethPrice, uint256 id); function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner { oracleAddress = _oracleInstanceAddress; oracleInstance = EthPriceOracleInterface(oracleAddress); emit newOracleAddressEvent(oracleAddress); } function updateEthPrice() public { uint256 id = oracleInstance.getLatestEthPrice(); myRequests[id] = true; emit ReceivedNewRequestIdEvent(id); } function callback(uint256 _ethPrice, uint256 _id) public { // 3. Continue here require(myRequests[_id], "This request is not in my pending list."); ethPrice = _ethPrice; delete myRequests[_id]; emit PriceUpdatedEvent(_ethPrice, _id); } } ``` ## onlyOracle Modifier 在實作完 callback function 之後 必須要確保只有 Oracale Contract 才能去呼叫 可以透過檢查 msg.sender 是否等於 oracleAddress 來實作一個 onlyOracle modifier ### 實作 onlyOracle Modifier 1. 宣告 modifer onlyOracle 並且放到 callback 之上 2. 第一行需要加以下邏輯來限定只有 Oracle Contract 可以呼叫 ```solidity= require(msg.sender == oracleAddress, "You are not authorized to call this function."); ``` 3. 最後放入 _; 代表繼續執行其他邏輯 ```solidity= pragma solidity 0.5.0; import "./EthPriceOracleInterface.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract CallerContract is Ownable { uint256 private ethPrice; EthPriceOracleInterface private oracleInstance; address private oracleAddress; mapping(uint256=>bool) myRequests; event newOracleAddressEvent(address oracleAddress); event ReceivedNewRequestIdEvent(uint256 id); event PriceUpdatedEvent(uint256 ethPrice, uint256 id); function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner { oracleAddress = _oracleInstanceAddress; oracleInstance = EthPriceOracleInterface(oracleAddress); emit newOracleAddressEvent(oracleAddress); } function updateEthPrice() public { uint256 id = oracleInstance.getLatestEthPrice(); myRequests[id] = true; emit ReceivedNewRequestIdEvent(id); } function callback(uint256 _ethPrice, uint256 _id) public onlyOracle { require(myRequests[_id], "This request is not in my pending list."); ethPrice = _ethPrice; delete myRequests[_id]; emit PriceUpdatedEvent(_ethPrice, _id); } modifier onlyOracle() { // Start here require(msg.sender == oracleAddress, "You are not authorized to call this function."); _; } } ``` ## 實作 getLatestEthPrice ### getLatestEthPrice function 為了讓呼叫者可以紀錄 Request, getLatestEthPrice 首先需要計算 request id 還有為了避免 id 被人盜用,這個 id 必須很難預測。 為了難以預測也許會需要使用隨機數 隨機數的作法可以透過 kecca256 加入時間戳 now 與 randNounce 來實作如下 ```solidity= uint randNonce = 0; uint modulus = 1000; uint randomNumber = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus; ``` 這個隨機數產生了 0 - 999 之間的隨機數。 ### 實作 1. 宣告 function getLatestEthPrice 回傳值: uint256 存取權限: public ```solidity= function getLatestEthPrice() public returns(uint256) { } ``` 2. 在第一行把 randNonce++ 3. 計算一個 0 到 modulus 的隨機數,並且把結果存到一個 uint id ```solidity= pragma solidity 0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./CallerContractInterface.sol"; contract EthPriceOracle is Ownable { uint private randNonce = 0; uint private modulus = 1000; mapping(uint256=>bool) pendingRequests; event GetLatestEthPriceEvent(address callerAddress, uint id); event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress); // Start here function getLatestEthPrice() public returns(uint256) { randNonce++; uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus; } } ``` ## 處理 pendingRequest 接下來必須實作一個簡單的系統來紀錄 pending request 可以透過 mapping 來紀錄這些 request 最後 getLatestEthPrice 會發送一個 event 來通知該 request id ### 實作處理 pendingRequest 邏輯 1. 建立 mapping(uint => bool) pendingRequests 2. 首先更新 pendingRequests[id]=true 3. emit GetLatestEthPriceEvent(msg.sender, id) 4. 最後回傳 id ```solidity= pragma solidity 0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./CallerContractInterface.sol"; contract EthPriceOracle is Ownable { uint private randNonce = 0; uint private modulus = 1000; mapping(uint256=>bool) pendingRequests; event GetLatestEthPriceEvent(address callerAddress, uint id); event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress); function getLatestEthPrice() public returns (uint256) { randNonce++; uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus; // Start here pendingRequests[id] = true; emit GetLatestEthPriceEvent(msg.sender, id); return id; } } ``` ## 實作 setLatestEthPrice 當透過 js 元件取得 ETH 價格後 ,最後會呼叫 setLatestEthPrice 這個 function 來傳送結果 setLatestEthPrice 主要會有以下參數 * ETH price * Caller 的 Contract Address * Request 的 id 首先必須要確認這個 function 只有 owner 可以呼叫 然後確認 request id 是否合法 如果合法最後要從 pendingRequests 移除掉 id ### 實作 1. 建立 public function setLatestEthPrice 需要3個參數: _ethPrice(uint256), _callerAddress(address), _id(uint256) 並且需要設定只有 owner 可以呼叫 2. 使用以下語法確認 _id 是合法的 ```solidity= require(pendingRequests[_id], "This request is not in my pending list."); ``` 3. 使用以下語法把 id 從 pendingRequests 中移除 ```solidity= delete pendingRequests[id]; ``` ```solidity= pragma solidity 0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./CallerContractInterface.sol"; contract EthPriceOracle is Ownable { uint private randNonce = 0; uint private modulus = 1000; mapping(uint256=>bool) pendingRequests; event GetLatestEthPriceEvent(address callerAddress, uint id); event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress); function getLatestEthPrice() public returns (uint256) { randNonce++; uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus; pendingRequests[id] = true; emit GetLatestEthPriceEvent(msg.sender, id); return id; } // Start here function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner { require(pendingRequests[_id], "This request is not in my pending list."); delete pendingRequests[_id]; } } ``` ## 呼叫 callback 接下來還有以下流程需要處理 * 初始化 CallerContractInstance * 透過 CallerContractInstance 呼叫 callback 並且傳入新的 ETH 價格與 Request id * 最後發送一個 event 通知前端 price 已經成功更新 ### 實作呼叫 callback 邏輯 1. 建立 CallerContractInterface 變數 callerContractInstance 2. 使用 Caller Contract address 來初始化 callerContractInstance 3. 執行 callerContractInstance.callback 傳入 _ethPrice 與 _id 4. emit SetLatestEthPriceEvent(_ethPrice, _callerAddress) ```solidity= pragma solidity 0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./CallerContractInterface.sol"; contract EthPriceOracle is Ownable { uint private randNonce = 0; uint private modulus = 1000; mapping(uint256=>bool) pendingRequests; event GetLatestEthPriceEvent(address callerAddress, uint id); event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress); function getLatestEthPrice() public returns (uint256) { randNonce++; uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus; pendingRequests[id] = true; emit GetLatestEthPriceEvent(msg.sender, id); return id; } function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner { require(pendingRequests[_id], "This request is not in my pending list."); delete pendingRequests[_id]; // Start here CallerContractInterface callerContractInstance; callerContractInstance = CallerContractInterface(_callerAddress); callerContractInstance.callback(_ethPrice, _id); emit SetLatestEthPriceEvent(_ethPrice, _callerAddress); } } ```