iT邦幫忙

2022 iThome 鐵人賽

DAY 17
0
Web 3

Road Map To DApp Developer系列 第 17

【DAY17】 - Dynamic NFT & Oracle (III) (ChainLink Data Feed & Upkeep)

  • 分享至 

  • xImage
  •  

Preface

上篇中介紹了 ChainLink 的 Oracle 是如何達到整理資料與確認資料無誤的機制,但是沒有提到如何在自己的 Smart Contract 中使用他們的程式來達到判斷的效果。因此今天會使用 ChainLink 的 Oracle 合約來達到從外部得到幣價資訊,並拿來改變我們的 NFT metadata。

Data Feed

ChainLink 設計的 Data Feed 系統採用的機制就如昨天提到的最後一個部分,在 DON 的節點會利 CHAIN-LINK-SC 寫好的合約來向外部 Source 取得想要的資訊。

而在 Data Feed 裡面有一個三角形:

  1. Consumer: 其實就是用戶端(USER-SC)。Consumer 需要在自己的合約中使用 AggregatorV3Interface (ChainLink 會更新他們的合約,目前是 version 3) 來呼叫 proxy 合約中的 function,透過與 proxy 互動的方式來得到想要的資訊。
  2. Proxy contract: 為中介層(CHAIN-LINK-SC)。Proxy 的合約是負責連接 Aggregator 與 Consumer 之間的橋樑,可以把 Aggregator 從外部取得的資料傳送給 Consumer。使用 Proxy 的另一個重要的原因是如此一來就可以更容易的更新 Aggregator 的內容。
  3. Aggregator contract: Aggregator 負責從 DON 中取得資料,並把這些資料儲存在鏈上。

Example

如何簡單的從 DON 中取得資料,可以看以下的例子。

若你想從 Goerli 測試鏈中取得當前的 ETH/USD 匯率,可以這樣寫。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract getPrice {
    AggregatorV3Interface internal priceFeed;
    constructor() {
        priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
    }
    
    function getLatestPrice() public view returns (int) {
        (   
            /*uint80 roundID*/,  
            int price,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = priceFeed.latestRoundData();
        return price;
    }
}

import 中使用了 AggregatorV3Interface,可以讓繼承他的合約自行定義裡面的 statement。

而在 constructor 中初始化了我們想要使用的合約。先前已經有一個人把 proxy contract 部屬在鏈上了,而0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e 這個地址就是一個 proxy contract,因此在我們 import AggregatorV3Interface 後我們就可以直接在我們的合約中呼叫 proxy contract 裡面的函式

最後在 getLatestPrice() 中呼叫了 priceFeed.lasestRoundData() 這個函式,這時便會呼叫 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e 中的 lastestRoundData(),隨後 Aggregator 便會連接數個 DON 上的節點,並取得資料並提供給 Proxy,最後再由 Proxy 回傳到資料(在 Goerli 上的 ETH/USD 匯率)我們的 getPrice 合約中。

而最後得到的資料長的像這樣:

Aggregator 是如何更新的

Aggregator 每一段時間會自動更新數據,而更新的條件有兩個:

  1. Deviation threshold: 當外部資料與鏈上資料的差距達到一定程度(某個 threshold)的時候會啟動更新,aggregator 將會開始新的一個 round。
  2. Heartbeat: 設定一段時間,當系統超過這段時間沒有更新的時候便會開始新的一 round。

Automate System

Oracle 的概念不只可以用在鏈外與鏈內的檔案傳輸,也可以應用在條件觸發或是自動化管理的合約上。Upkeep 是 ChainLink 設計的一個另一個 Oracle 系統,它的原理跟 Data Feed 類似。在 Keeper 這個系統中有三個角色:

  1. Upkeep: 就是使用者自定義的動作(在鏈上運行的 statement),
  2. Keeper registry: 類似於管理一個 contract 中 upkeep 的角色。
  3. Keepers: 在 Keeper network 中的節點,負責執行被 register 與 funded 的 Upkeep(使用者需要付出一定的 LINK 來執行 upkeep)

Example

這裡拿一個在 chainlink 官網上的簡單例子來說明:

import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";

contract Counter is KeeperCompatibleInterface {
    uint public counter;
    
    uint public immutable interval; // 我們決定的「一段時間」
    uint public lastTimeStamp; // 用來記錄 deploy 或者後來更新的時間戳記
    
    constructor(uint updateInterval) {
        interval = updateInterval;
        lastTimeStamp = block.timestamp;
        
        counter = 0;
    }
    
    function checkUpkeep(bytes calldata /*checkData*/) external view override returns (bool upkeepNeeded, bytes memory /*performData*/) {
        upkeepNeeded = (block.timestamp - lastTimeStamp) > interval; 
        // 會確認時間是不是經過了 interval 
    }
    
    function performUpkeep(bytes calldata /* performData */) external override {
        if ((block.timestamp - lastTimeStamp) > interval ) {
            lastTimeStamp = block.timestamp;
            counter = counter + 1;
        }
    }
    
}

首先要繼承 KeeperCompatibleInterface,如此才能 override 其中的函式。

KeeperCompatible 中有兩個特殊的 function,checkUpkeep()performUpkeep()
簡單地來說,這兩個 function 其中一個是負責要確認條件是否發生,另一個則是負責來執行你想要做的事情

  1. checkUpkeep(checkData) -> boolean, bytes
    • 這個函式會利用 eth_call (可以不需要透過交易 (transaction) 來達到 message call 的方法)的方法,在每一次新的以太鏈區塊產生時會執行其中的 statement。
    • 傳入的 checkData 是由 chainLink 在註冊 Keeper 的時候需要設定的值,實際流程與說明詳情請看這裡。傳入後可以在 function 中幫助判斷。
    • return 的值有二: 第一個是個 boolean,如果是 true 的話就會執行 performUpkeep();第二個則是 bytes,是用來傳入 performUpkeep() 時的資料。
  2. performUpkeep(performData)
    • checkUpkeep() return 的 boolean(upkeepNeeded) == true 時,在 Keeper 系統裡的節點會發起一個交易到區塊鏈上,這個交易便是使用 performData 作為 input 來呼叫 performUpkeep()

如果把上述兩者關係用一張簡圖來表示會長得像:

以上面的例子而言,checkUpkeep() 會在每次新的區塊產出時確認「目前時間戳記」與「上次紀錄的時間戳記」相差是否超過 interval 的值,如果超過了便會讓 counter += 1

Implementation

依照上述的方法,我實作了一個簡單的例子,大致流程像是這樣:

主要有改動的程式大概在這邊:

    function safeMint(address to, uint256 tokenId) public {
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, dog);
    }
    
    function checkUpkeep(bytes calldata /*checkData*/) external view override returns (bool upkeepNeeded, bytes memory /*performData*/) {
        int currentPrice = getEthPrice();
        upkeepNeeded = ( (currentPrice - lastPrice)  > 0 );
    }
    
    function performUpkeep(bytes calldata /* performData */) external override {
        int currentPrice = getEthPrice();
        if ( (currentPrice - lastPrice) > 0 ) {
            lastPrice = currentPrice;
            _setTokenURI(0, cat);
        }
    }

想看全部程式碼可見這裡

透過 upkeep 系統,可以看到很快的 NFT 的 URI 已經改變了。

小結

今天實作了一個可以根據 ChainLink 上的 Data Feed 得到的幣價來更新 NFT URI 的方法!而在搜尋資訊時,為了了解背後的 Oracle 機制真的花了蠻多的時間,但目前有很多的項目方或是 Defi 背後運行的 Oracle 都是使用 ChainLink,因此的確是值得學習的,希望我可以在 project 中使用其機制來實作出一些自動化的功能。

References


若有文章內有任何錯誤的地方歡迎指點與討論!非常感謝!

歡迎贊助窮困潦倒大學生
0xd8538ea74825080c0c80B9B175f57e91Ff885Cb4


上一篇
【DAY16】 - Dynamic NFTs & Oracle (Chainlink Oracle)
下一篇
【DAY18】 - White list
系列文
Road Map To DApp Developer30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言