1 Chainlink PriceFeeds 基本使用
PriceFeeds主要用于為Defi(去中心化金融)項目提供鍊下的資産價格參考,也就是說使用者或項目方可以通過Chainlink的PriceFeeds合約擷取到鍊下的資産價格資料。
Chainlink 官網給了一個PriceFeeds的使用例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumerV3 {
AggregatorV3Interface internal priceFeed;
/**
* Network: Goerli
* Aggregator: ETH/USD
* Address: 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e
*/
constructor() {
priceFeed = AggregatorV3Interface( //執行個體化AggregatorV3Interface接口
0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e //指向ETH/USD價格參考合約
);
}
/**
* Returns the latest price
*/
function getLatestPrice() public view returns (int) {
(
uint80 roundID, //目前喂價輪次的ID
int price, //ETH/USD币對價格
uint startedAt, //本輪喂價開始的時間戳
uint timeStamp, //獲得最終聚合價格的時間戳
uint80 answeredInRound //價格被計算出來時的輪次ID
) = priceFeed.latestRoundData();
return price;
}
}
PriceFeeds官方示例代碼
Chainlink在各條公鍊生态上為每個資産币對分别建立了一個價格參考合約,當我們需要獲得特定資産的價格資料時,隻需要在constructor()函數中将接口執行個體化的位址修改成對應資産的價格參考合約位址然後将合約部署上鍊,再調用getLastestPrice()函數即可擷取到相應的資産價格。各種資産的參考價格合約位址可以在Chainlink官網進行查詢。
參考價格合約位址查詢網站
官網資産價格參考合約位址示意圖
2 PriceFeeds合約分析
在上面官方給的樣例合約中可以看到擷取價格資料的功能由價格參考合約接口AggregatorV3Interface實作,樣例合約隻是調用了價格參考合約裡的latestRoundData()函數,是以若需要對PriceFeeds的鍊上合約部分進行分析,我們就需要進入到價格參考合約中看latestRoundData()的具體實作。
為了了解PriceFeeds功能在生産環境中的具體實作,我們直接找一個在以太坊主網中真正提供服務的價格參考合約進行分析。
BTC/ETH價格參考合約位址
這裡選的是主網中的BTC/ETH價格參考合約,我們在以太坊區塊鍊浏覽器中搜尋該合約位址,點選“Contract”按鈕即可檢視合約源碼。價格參考合約在以太坊區塊鍊浏覽器中的合約名稱為“EACAggregatorProxy”。
主網BTC/ETH價格參考合約位址:0xdeb288F737066589598e9214E782fa5A8eD689e8
etherscan中的合約檢視頁面
由于合約源碼較長,下面隻截取相關的部分合約實作源碼進行描述。
2.1 EACAggregatorProxy合約
/**
* @title External Access Controlled Aggregator Proxy
* @notice A trusted proxy for updating where current answers are read from
* @notice This contract provides a consistent address for the
* Aggregator and AggregatorV3Interface but delegates where it reads from to the owner, who is
* trusted to update it.
* @notice Only access enabled addresses are allowed to access getters for
* aggregated answers and round information.
*/
contract EACAggregatorProxy is AggregatorProxy { //繼承AggregatorProxy合約
AccessControllerInterface public accessController; //定義一個合約接口類型
constructor(
address _aggregator, //聚合器合約位址
address _accessController //權限控制位址,用于權限判定
)
public
AggregatorProxy(_aggregator) //執行個體化父合約
{
setController(_accessController);
}
/**
* @notice Allows the owner to update the accessController contract address.
* @param _accessController The new address for the accessController contract
*/
function setController(address _accessController)
public
onlyOwner()
{
//執行個體化權限控制合約
accessController = AccessControllerInterface(_accessController);
}
}
價格參考合約EACAggregatorProxy初始化代碼
先來看看價格參考合約的一些基本定義,我們可以看到價格參考合約EACAggregatorProxy繼承了父合約AggregatorProxy,并且在構造函數中将聚合器合約作為傳入參數将父合約執行個體化,然後調用setController函數将權限控制合約執行個體化并将合約對象賦予accessController。
interface AccessControllerInterface {
function hasAccess(address user, bytes calldata data) external view returns (bool);
}
權限控制合約接口
權限控制合約accessController是通過接口AccessControllerInterface執行個體化的,而該接口中隻有一個hasAccess函數,入參是名為user的位址類型以及一個calldata,初步判定這個函數的功能是判斷某個位址是否有權限。
/**
* @notice get data about the latest round. Consumers are encouraged to check
* that they're receiving fresh data by inspecting the updatedAt and
* answeredInRound return values.
* Note that different underlying implementations of AggregatorV3Interface
* have slightly different semantics for some of the return values. Consumers
* should determine what implementations they expect to receive
* data from and validate that they can properly handle return data from all
* of them.
* @return roundId is the round ID from the aggregator for which the data was
* retrieved combined with a phase to ensure that round IDs get larger as
* time moves forward.
* @return answer is the answer for the given round
* @return startedAt is the timestamp when the round was started.
* (Only some AggregatorV3Interface implementations return meaningful values)
* @return updatedAt is the timestamp when the round last was updated (i.e.
* answer was last computed)
* @return answeredInRound is the round ID of the round in which the answer
* was computed.
* (Only some AggregatorV3Interface implementations return meaningful values)
* @dev Note that answer and updatedAt may change between queries.
*/
function latestRoundData()
public
view
checkAccess()
override
returns (
uint80 roundId, //聚合器進行資料聚合的輪次ID
int256 answer, //最終聚合得到的價格資料
uint256 startedAt, //聚合開始的時間戳
uint256 updatedAt, //聚合結束的時間戳(算出最終answer并更新的時間戳)
uint80 answeredInRound //answer被計算出來時的輪次ID
)
{
return super.latestRoundData();
}
價格參考合約的latestRoundData()函數實作
官方示例中的合約就是調用了價格參考合約的latestRoundData函數擷取價格資料的,我們可以看到latestRoundData函數調用了一個函數修飾器checkAccess用于權限判斷,判斷調用者是否有擷取價格資料的權限。而在函數的實作中,該函數通過super字段調用了父合約的latestRoundData函數來擷取相應的價格資料。是以我們需要進一步檢視checkAccess和父合約AggregatorProxy的實作。
先看看checkAccess修飾器的實作:
modifier checkAccess() {
AccessControllerInterface ac = accessController;
require(address(ac) == address(0) || ac.hasAccess(msg.sender, msg.data), "No access");
_;
}
價格參考合約中的checkAccess函數修飾器
修飾器checkAccess中執行個體化了一個ac用于接收權限控制合約accessController,然後該修飾器判斷通過的條件是當ac合約位址為0位址或者是使用該修飾器的函數的調用者位址能夠通過ac合約内hasAccess函數的權限認證。是以若accessController為0位址,即沒有執行個體化權限控制合約時,checkAccess能無條件通過,否則則需要通過hasAccess的權限認證判斷調用者是否有權限。
為了了解Chainlink在checkAccess修飾器中所作的權限控制,我們需要到權限控制合約中檢視具體實作。
etherscan中accessController變量的最新值
在etherscan中可以直接檢視已上鍊合約中變量的最新值,我們可以在BTC/ETH價格參考合約中通過檢視該合約中accessController合約對象的位址找到Chainlink部署上鍊的權限控制合約,但是檢視完之後發現目前accessController為0位址,這也就意味着Chainlink目前并沒有給價格參考合約設定權限,任何位址都能通過checkAccess的權限判斷。
既然不能通過這裡找到權限控制合約的源碼,那我們也可以通過github尋找Chainlink項目方的開源代碼庫,并從中尋找權限控制合約。
/**
* @title SimpleWriteAccessController
* @notice Gives access to accounts explicitly added to an access list by the
* controller's owner.
* @dev does not make any special permissions for externally, see
* SimpleReadAccessController for that.
*/
contract SimpleWriteAccessController is AccessControllerInterface, ConfirmedOwner {
bool public checkEnabled;
mapping(address => bool) internal accessList;
constructor() ConfirmedOwner(msg.sender) {
checkEnabled = true;
}
/**
* @notice Returns the access of an address
* @param _user The address to query
*/
function hasAccess(address _user, bytes memory _calldata) public view virtual override returns (bool) {
return accessList[_user] || !checkEnabled;
}
/**
* @title SimpleReadAccessController
* @notice Gives access to:
* - any externally owned account (note that off-chain actors can always read
* any contract storage regardless of on-chain access control measures, so this
* does not weaken the access control while improving usability)
* - accounts explicitly added to an access list
* @dev SimpleReadAccessController is not suitable for access controlling writes
* since it grants any externally owned account access! See
* SimpleWriteAccessController for that.
*/
contract SimpleReadAccessController is SimpleWriteAccessController {
/**
* @notice Returns the access of an address
* @param _user The address to query
*/
function hasAccess(address _user, bytes memory _calldata) public view virtual override returns (bool) {
return super.hasAccess(_user, _calldata) || _user == tx.origin;
}
}
權限控制合約部分代碼
權限控制合約為SimpleReadAccessController,其繼承了父合約SimpleWriteAccessController,SimpleReadAccessController中的hasAccess函數中調用到了父合約的hasAccess函數,我們先來看看父合約中的hasAccess函數。
父合約的hasAccess函數的入參是address類型的_user以及一個bytes類型的calldata,傳回值則是bool類型。該函數根據傳入的位址是否在accessList權限清單中來判斷該位址是否有通路權限,而checkEnabled則是一個判斷開關,當checkEnabled被設定為false時hasAccess無條件傳回true,是以實際上父合約中的hasAccess就是一個白名單查詢函數。這裡可以看到傳入的calldata并沒有被使用,推測可能留作後續合約更新。
接在再來看看子合約SimpleReadAccessController中的hasAccess函數。這個hasAccess函數除了調用父合約的hasAccess函數判斷白名單權限外,還對user是否為tx.origin進行判斷,當調用hasAccess的位址就是這次交易的最初發起者時該函數也傳回true,這也就意味着當外部個人賬戶直接與價格參考合約互動擷取價格資料時也能通過checkAccess修飾器的權限判斷。是以在用含有hasAccess的checkAccess修飾器進行權限判斷時,隻有外部個人賬戶位址或在白名單内的位址可以通過權限認證,而若是其他DeFi項目想要通過價格參考合約擷取資産價格資料,則需要先聯系Chainlink官方将該DeFi項目中的合約位址加入白名單。
當然前面也提到價格參考合約中的accessController合約目前是0位址,也就說明目前Chainlink官方并沒有在一些主流資産價格參考合約上設定權限控制進行如上面所描述的權限判斷,個人推斷可能在一些DeFi項目因需要一些非主流資産币對的鍊下價格資料找Chainlink團隊定制資料聚合服務時會啟用該權限控制合約。
2.2 AggregatorProxy合約
分析完了checkAccess權限控制的實作,現在還需要看看價格參考合約的父合約AggregatorProxy中的latestRoundData函數實作。
/**
* @notice get data about the latest round. Consumers are encouraged to check
* that they're receiving fresh data by inspecting the updatedAt and
* answeredInRound return values.
* Note that different underlying implementations of AggregatorV3Interface
* have slightly different semantics for some of the return values. Consumers
* should determine what implementations they expect to receive
* data from and validate that they can properly handle return data from all
* of them.
* @return roundId is the round ID from the aggregator for which the data was
* retrieved combined with an phase to ensure that round IDs get larger as
* time moves forward.
* @return answer is the answer for the given round
* @return startedAt is the timestamp when the round was started.
* (Only some AggregatorV3Interface implementations return meaningful values)
* @return updatedAt is the timestamp when the round last was updated (i.e.
* answer was last computed)
* @return answeredInRound is the round ID of the round in which the answer
* was computed.
* (Only some AggregatorV3Interface implementations return meaningful values)
* @dev Note that answer and updatedAt may change between queries.
*/
function latestRoundData()
public
view
virtual
override
returns (
uint80 roundId, //聚合器進行資料聚合的輪次ID
int256 answer, //最終聚合得到的價格資料
uint256 startedAt, //聚合開始的時間戳
uint256 updatedAt, //聚合結束的時間戳(算出最終answer并更新的時間戳)
uint80 answeredInRound //answer被計算出來時的輪次ID
)
{
Phase memory current = currentPhase; // cache storage reads
(
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 ansIn
) = current.aggregator.latestRoundData(); //從current.aggregator中擷取傳回值
return addPhaseIds(roundId, answer, startedAt, updatedAt, ansIn, current.id);
}
latestRoundData函數在AggregatorProxy父合約中的實作
父合約AggregatorProxy中latestRoundData函數中定義了一個Phase結構體current,并從currentPhase中接收值,這裡currentPhase是該合約的storage存儲類型資料,也就是全局狀态變量。current從currentPhase中接收到值後,roundId、answer、startedAt、updatedAt和ansIn這5個參數就從current.aggregator的latestRoundData()函數中擷取傳回值,是以我們可以判斷current.aggregator是負責聚合價格資料的聚合器合約。
目前面的步驟都完成後,最後latestRoundData()函數會調用addPhaseIds對前面擷取的5個傳回參數進行二次處理并最終傳回。
2.2.1 Phase結構體
struct Phase {
uint16 id; //該Phase的id号
AggregatorV2V3Interface aggregator; //該Phase的聚合器合約(接口執行個體化)
}
Phase private currentPhase; //用于存儲最新的Phase
AggregatorV2V3Interface public proposedAggregator; //用于提議新的聚合器合約
//Phase結構體中id到對應聚合器合約的映射 phaseAggregators,友善根據id查找對應階段的聚合器
mapping(uint16 => AggregatorV2V3Interface) public phaseAggregators;
Phase結構體定義
先來看看Phase結構體的定義,Phase結構體内定義了一個id和一個AggregatorV2V3Interface合約接口aggregator,在執行個體化Phase結構體的時候項目方會傳入一個聚合器合約的位址和其對應的id,用于辨別某個Phase的id及該Phase對應使用的聚合器合約,現在我們暫且把Phase了解為階段。當把一個聚合器合約位址傳給aggregator時aggregator會用AggregatorV2V3Interface接口對合約進行執行個體化,用以表示這個位址所對應的運作在以太坊虛拟機中的合約對象。
定義完Phase結構體後緊接着用這個結構體聲明一個全局私有變量currentPhase,用于辨別目前最新階段的id和該階段的聚合器合約。
同時該合約還定義了兩個變量,一個是聚合器接口類型proposedAggregator,這個變量主要是用于提議新的聚合器合約;另一個是mapping類型的phaseAggregators,這個變量主要是存儲從階段id到該階段所對應的聚合器合約的映射,便于根據id查找對應階段的聚合器合約。
/*
* Internal
*/
function setAggregator(address _aggregator)
internal
{
uint16 id = currentPhase.id + 1; //id自增
//更新currentPhase
currentPhase = Phase(id, AggregatorV2V3Interface(_aggregator));
//将最新階段的資訊存入phaseAggregators
phaseAggregators[id] = AggregatorV2V3Interface(_aggregator);
}
setAggregator函數實作
合約中用一個setAggregator函數來設定currentPhase,傳入的參數是目前階段的聚合器合約位址。每當需要設定一個新的currentPhase時,setAggregator函數都會将更新前的currentPhase的id加1,然後連同傳入的參數aggregator一起将值傳給currentPhase以對currentPhase進行更新。最後新生成的currentPhase的相關資訊會被存進phaseAggregators映射中。
從這裡我們可以得知每個階段的id是遞增的。
/**
* @notice Allows the owner to propose a new address for the aggregator
* @param _aggregator The new address for the aggregator contract
*/
//提議一個聚合器合約
function proposeAggregator(address _aggregator)
external
onlyOwner()
{
proposedAggregator = AggregatorV2V3Interface(_aggregator);
}
/**
* @notice Allows the owner to confirm and change the address
* to the proposed aggregator
* @dev Reverts if the given address doesn't match what was previously
* proposed
* @param _aggregator The new address for the aggregator contract
*/
//确認在最新的階段使用已經提議的聚合器合約
function confirmAggregator(address _aggregator)
external
onlyOwner()
{
require(_aggregator == address(proposedAggregator), "Invalid proposed aggregator"); //判斷傳入的要确認的位址是否是已經提議的聚合器合約位址,不是的話則報錯
delete proposedAggregator; //位址确認完成,删除提議的聚合器合約
setAggregator(_aggregator); //确認提議的聚合器合約,生成對應的currentPhase
}
進行聚合器合約替換的部分代碼
我們可以看到前面的setAggregator函數的可見性是internal,這也就意味着這個函數會在合約裡的其它地方被調用。對合約進行查找後,能夠發現proposeAggregator和confirmAggregator函數與設定新階段的聚合器合約有關系。
proposeAggregator函數用于提議一個新的聚合器合約,confirmAggregator合約則是對這個提議進行确認,我們可以看到當對提議進行确認時,儲存提議的聚合器合約對象會被删除,然後再調用setAggregator函數對currentPhase進行更新,currentPhase中的aggregator字段就存儲着已确認的最新的聚合器合約對象。
proposeAggregator和confirmAggregator兩個函數的可見性都是external,是因為這兩個函數是用于被外部調用的,當價格參考合約的管理者想要用新的聚合器合約去聚合價格資料時,就用從外部調用這兩個函數。
就目前掌握的資訊而言,還不知道Phase中的Id是否與roundId有聯系,也不确實是否每一輪資料聚合都對應着一個新的Phase,是否每一輪資料聚合都會換一個聚合器合約,是以我們可以去etherscan檢視currentPhase目前最新的狀态輔助我們進行判斷。
currentPhase的ID
可以看到BTC/ETH價格參考合約中currentPhase目前的id是4,也就是說從該合約上鍊到現在其用于聚合資料的聚合器合約隻被更換了4次,是以我們可以判斷聚合器合約的更換與資料聚合輪次沒有聯系,可能是當Chainlink官方需要對聚合器合約的業務進行更新的時候才會替換聚合器合約。是以相比于将Phase翻譯為階段,這裡翻譯成版本會更合适。
2.2.2 addPhaseIds函數
2.2.1部分主要是對BTC/ETH價格參考合約中的Phase結構體和currentPhase變量及其相應的操作函數進行分析描述,而在價格參考合約的latestRoundData函數中在return時還調用了addPhaseIds函數對傳回值進行了處理,2.2.2部分會對addPhaseIds函數進行展開。
function addPhaseIds(
uint80 roundId, //聚合器進行資料聚合的輪次ID
int256 answer, //最終聚合得到的價格資料
uint256 startedAt, //聚合開始的時間戳
uint256 updatedAt, //聚合結束的時間戳(算出最終answer并更新的時間戳)
uint80 answeredInRound, //answer被計算出來時的輪次ID
uint16 phaseId //currentPhase中的id,與聚合器聚合資料的輪次id不同
)
internal
view
returns (uint80, int256, uint256, uint256, uint80)
{
return (
addPhase(phaseId, uint64(roundId)),
answer,
startedAt,
updatedAt,
addPhase(phaseId, uint64(answeredInRound))
);
}
addPhaseIds函數實作
在2.2開頭父合約AggregatorProxy的latestRoundData函數中我們可以看到roundId、answer、startedAt、updatedAt和ansIn這5個參數的值是在currentPhase結構體中的聚合器合約裡擷取的,而addPhaseIds函數的作用則是對這些參數進行二次加工,并且currentPhase的id也被作為入參傳了進去。
從上面addPhaseIds函數的具體實作中我們可以看到answer、startedAt和updatedAt三個參數被原封不動地return了回去,隻有roundId和answeredInRound以及phaseId三個參數被addPhase函數進行了處理。也就是說,最後傳回給使用者的latestRoundData函數中的roundId是addPhase(phaseId, uint64(roundId)),answeredInRound則是addPhase(phaseId, uint64(answeredInRound))。
function addPhase(
uint16 _phase,
uint64 _originalId
)
internal
view
returns (uint80)
{
//用位運算拼接phaseId和roundId
return uint80(uint256(_phase) << PHASE_OFFSET | _originalId); //位運算
}
addPhase函數實作
是以再來看看addPhase函數,該函數的入參是phaseId以及roundId或answerInRound,其中傳入roundId或answerInRound隻截取了右邊的64位。addPhase函數在對兩個入參進行位運算後将結果傳回,其中<<是左偏移運算,a<<b的意思就是将a的二進制全部向左偏移b位,超出左邊的部分直接舍棄,右邊空出的部分補0,a<<b的本質是通過将a乘以2的b次方來對a向左進行偏移,不過這是二進制運算的知識點這裡不做展開;|是按位或運算,比如二進制11001和10011的|運算就是兩個數從左到右分别按位進行或運算,當兩個數字對應位置有任意一個1時,則結果中對應的位置為1,是以這兩個數的|運算結果為11011。
因為<<運算符的優先級高于|,是以addPhase函數中是先将phase向左偏移PHASE_OFFSET位然後再與originalId按位進行或運算。
uint256 constant private PHASE_OFFSET = 64;
PHASE_OFFSET變量
合約中PHASE_OFFSET被設定為64,是以當phase向左偏移後右邊會補64個0(phase原長度為16為,是以需要将它轉換成256位的uint256才有位置進行偏移),而originalId的長度也為64位,當originalId與phase右邊的64個0進行或運算時結果就是originalId本身,是以addPhase函數中位運算的意義就是将長度位64位元組的originalId拼接在長度位16的phaseId的右邊,最後傳回一個長度為80個位元組的uint80資料。
為了更加清晰地表達這個過程,可以簡單地畫個圖(長度都進行了縮減):
addPhase位運算示意圖
當位運算結束後,uint256(_phase) << PHASE_OFFSET | _originalId的長度為256,而有實際意義的長度為80,是以最後再強制轉換成uint80。(從uint256轉為uint80會保留右邊80位)
是以最後傳回給latestRoundData()函數的roundId由phaseId和聚合器合約傳回的roundId的右邊64位拼接而成,answerInRound則是将PhaseId和聚合器合約傳回的answerInRound的右邊64位進行拼接後獲得。
為了驗證上面關于addPhase()函數分析的正确性,我們可以看看目前最新的latestRoundData()傳回的roundId:
latestRoundData函數傳回的roundId值
最新一輪價格聚合的roundId為73786976294838207617,而前面我們已經知道currentPhase.Id為4,4的二進制表達左移64位後的十進制為73786976294838206464,是以我們可以得知roundId是currentPhase.Id左移64位後加上1153獲得,則1153即為聚合器合約傳回的roundId,符合addPhase函數應該傳回的結果。
同時從上面latestRoundData函數傳回的值中可以看到roundId和answeredInRound是一樣的,這也就意味着價格聚合器從開始進行價格聚合到最後得出結果都在同一個輪次内。
當然addPhase函數的分析是否正确還得檢視聚合器合約傳回的roundId是否真為1153,是以接着往下看。
2.2.3 currentPhase.aggregator 聚合器合約
從2.2開頭的代碼中我們可以得知最終核心的價格資料是從聚合器合約中的latestRoundData函數中獲得的,價格參考合約的主要作用是可以選擇不同的聚合器合約來提供價格資料,以及對從聚合器合約獲得的價格資料進行處理等,是以我們還需要分析一下聚合器合約的具體實作。
聚合器合約位址
在etherscan上可以直接看到價格合約中的currentPhase.aggregator位址為0x81076d6Ff2620Ea9Dd7bA9c1015f0d09A3A732E6,我們根據這個位址來檢視聚合器合約的代碼以及合約最新的狀态。
聚合器合約名稱
聚合器合約名稱為AccessControllerOffchainAggregator,光看名稱就可以判斷出這個合約和EACAggregatorProxy合約一樣都是封裝在業務合約外面用于權限判斷的代理合約。
2.3 AccessControlledOffchainAggregator合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.1;
import "./OffchainAggregator.sol";
import "./SimpleReadAccessController.sol";
/**
* @notice Wrapper of OffchainAggregator which checks read access on Aggregator-interface methods
*/
contract AccessControlledOffchainAggregator is OffchainAggregator, SimpleReadAccessController {
/// @inheritdoc OffchainAggregator
function latestRoundData()
public
override
view
checkAccess() //在SimpleWriteAccessController合約中實作
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return super.latestRoundData();
}
}
AccessControlledOffchainAggregator合約部分代碼
AccessControlledOffchainAggregator合約繼承了父合約OffchainAggregator和SimpleReadAccessController,在2.1中我們已經得知SimpleReadAccessController合約繼承了其父合約SimpleWriteAccessController,主要的作用就是進行通路控制,判斷msg.sender是否在合約的白名單内。
而AccessControlledOffchainAggregator合約中的latestRoundData函數則是調用了其父合約OffchainAggregator的同名函數來擷取價格資料,并在調用前用修飾器checkAccess判斷調用該函數的合約位址是否在權限控制合約的白名單内。
SimpleWriteAccessController合約中的權限判斷函數
在etherscan中我們也可以看到當調用權限控制合約的hasAccess函數判斷價格參考合約的權限時傳回true,隻有在白名單内的合約才能從聚合器合約擷取價格資料。
2.4 OffchainAggregator父合約
OffchainAggregator合約就是從Chainlink網絡擷取價格資料的聚合器合約。
/**
* @notice aggregator details for the most recently transmitted report
* @return roundId aggregator round of latest report (NOT OCR round)
* @return answer median of latest report
* @return startedAt timestamp of block containing latest report
* @return updatedAt timestamp of block containing latest report
* @return answeredInRound aggregator round of latest report
*/
function latestRoundData()
public
override
view
virtual
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
roundId = s_hotVars.latestAggregatorRoundId;
// Skipped for compatability with existing FluxAggregator in which latestRoundData never reverts.
// require(roundId != 0, V3_NO_DATA_ERROR);
//根據roundId查找相應的價格資料鍊下報告(也就是Transmission)
Transmission memory transmission = s_transmissions[uint32(roundId)];
return (
roundId,
transmission.answer, //從transmission中擷取最後的answer
transmission.timestamp,
transmission.timestamp,
roundId //直接将roundId的值傳回給answeredInRound
);
}
OffchainAggregator合約中的latestRoundData函數
從上面的函數中我們可以看到roundId是從s_hotVars結構體中獲得的,而其他價格資料則是從transmission結構體和s_transmissions映射中獲得,是以接下來我們需要分析上面三個變量。
值得一提的是這裡的latestRoundData函數在最後傳回值的時候直接将roundId作為answeredInRound傳回,是以我們在2.2.2節的最後在etherscan中檢視傳回值時roundId和answeredInRound才會一緻。
OffchainAggregator合約中的latestRoundData函數傳回值
在etherscan查詢該合約的latestRoundData函數傳回值後可以看到roundId和answeredInRound傳回值一緻。同時roundId為1153,證明2.2.1和2.2.2關于addPhase函數的分析正确。
2.4.1 s_hotVars結構體
// Storing these fields used on the hot path in a HotVars variable reduces the
// retrieval of all of them to a single SLOAD. If any further fields are
// added, make sure that storage of the struct still takes at most 32 bytes.
struct HotVars {
// Provides 128 bits of security against 2nd pre-image attacks, but only
// 64 bits against collisions. This is acceptable, since a malicious owner has
// easier way of messing up the protocol than to find hash collisions.
// 最新的資料聚合配置參數,出于安全考慮用于對抗碰撞
bytes16 latestConfigDigest;
// 最新一輪所處的階段和輪次,前32位元組代表階段,後8個位元組用于表明輪次
uint40 latestEpochAndRound; // 32 most sig bits for epoch, 8 least sig bits for round
// Current bound assumed on number of faulty/dishonest oracles participating
// in the protocol, this value is referred to as f in the design
// 預言機網絡中能容忍的不誠實節點或錯誤節點的最大數量門檻值
uint8 threshold;
// Chainlink Aggregators expose a roundId to consumers. The offchain reporting
// protocol does not use this id anywhere. We increment it whenever a new
// transmission is made to provide callers with contiguous ids for successive
// reports.
// 最新一輪資料聚合的輪次ID
uint32 latestAggregatorRoundId;
}
HotVars internal s_hotVars; //HotVars結構體的執行個體化s_hotVars
HotVars結構體定義
從HotVars結構體的定義中我們可以看到幾個變量,其中最重要的就是latestAggregatorRoundId,該變量表示的是最新一輪資料聚合所處的輪次,latestRoundData在擷取最新的價格資料時就是根據這裡的輪次ID擷取。
s_hotVars是HotVars結構體的執行個體化。
/**
* @notice immediately requests a new round
* @return the aggregatorRoundId of the next round. Note: The report for this round may have been
* transmitted (but not yet mined) *before* requestNewRound() was even called. There is *no*
* guarantee of causality between the request and the report at aggregatorRoundId.
*/
function requestNewRound() external returns (uint80) {
require(msg.sender == owner || s_requesterAccessController.hasAccess(msg.sender, msg.data),
"Only owner&requester can call"); //權限控制,隻有管理者或聚合器合約擁有者才能發起新的輪次請求
HotVars memory hotVars = s_hotVars;
emit RoundRequested( //廣播新的輪次需求事件,預言機在接收到新的事件後就開始聚合資料
msg.sender,
hotVars.latestConfigDigest,
uint32(s_hotVars.latestEpochAndRound >> 8),
uint8(s_hotVars.latestEpochAndRound)
);
return hotVars.latestAggregatorRoundId + 1; //傳回新請求的id數
}
requestNewRound函數
當預言機網絡需要發起一輪新的資料聚合時就會從鍊下調用requestNewRound函數,該函數會向外界廣播新輪次的請求事件,然後傳回s_hotVars結構體中latestAggregatorRoundId+1,而當鍊外的預言機在監聽到新的事件時就會開始新一輪的資料聚合。
需要注意的是當requestNewRound函數被調用後latestAggregatorRoundId實際上還沒有+1,隻有當鍊下完成價格資料聚合并将鍊下報告上鍊後該值才會更新。
我們可以看在該函數在開頭判斷了調用者是否有發起新輪次請求的權限,用的還是hasAccess函數,也就是說判斷權限的邏輯一緻,但是我現在想知道這裡用的權限控制合約和之前是不是同一個合約。
為了便于區分,我将該函數所使用的權限控制合約命名為“新輪次請求權限判斷合約”,2.3中的權限控制合約命名為“價格參考權限控制合約”。(從實作層面來看本質上這兩個合約都是SimpleWriteAccessController合約)
新輪次請求權限判斷合約位址
在etherscan中可以檢視到新輪次請求權限判斷合約位址為0x641B698aD1C6E503470520B0EeCb472c0589dfE6,而2.3節中的價格參考權限控制合約是被子合約AccessControlledOffchainAggregator直接繼承然後才被部署到鍊上的,也就是說聚合器子合約中的權限控制合約位址與其自身一緻,為0x81076d6Ff2620Ea9Dd7bA9c1015f0d09A3A732E6。
是以聚合器的父合約和子合約所使用的權限控制合約雖然判斷邏輯一緻,但是卻是兩份不同的合約,其權限控制的目的也不一緻。
新輪次請求權限判斷合約中的權限判斷
此時用新輪次請求權限判斷合約的權限判斷函數查詢價格參考合約的權限時傳回的是false(2.3中傳回的是true)。
2.4.2 s_transmissions結構體映射及transmit函數
在2.4的latestRoundData函數中可以看到價格資料會被存放在Transmission結構體中,而每一輪得到的Transmission結構體都會被存入s_transmissions結構體數組,并以roundId為索引。
// Transmission records the median answer from the transmit transaction at
// time timestamp
struct Transmission {
int192 answer; // 192 bits ought to be enough for anyone
uint64 timestamp; //時間戳
}
//存放每一輪的Transmission,以roundId為索引
mapping(uint32 /* aggregator round ID */ => Transmission) internal s_transmissions;
Transmission和s_transmissions定義
在Transmission結構體中隻定義了兩個變量,一個是answer最終的價格資料answer,另一個則是獲得這個資料的時間戳。而s_transmissions則是從roundId到Transmission的mapping映射,可以根據id找到對應輪次的價格。
/**
* @notice transmit is called to post a new report to the contract
* @param _report serialized report, which the signatures are signing. See parsing code below for format. The ith element of the observers component must be the index in s_signers of the address for the ith signature
* @param _rs ith element is the R components of the ith signature on report. Must have at most maxNumOracles entries
* @param _ss ith element is the S components of the ith signature on report. Must have at most maxNumOracles entries
* @param _rawVs ith element is the the V component of the ith signature
*/
function transmit(
// NOTE: If these parameters are changed, expectedMsgDataLength and/or
// TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly
// 鍊下報告内容
bytes calldata _report,
// 報告附帶的預言機節點的簽名
bytes32[] calldata _rs, bytes32[] calldata _ss, bytes32 _rawVs // signatures
)
external
{
// 擷取目前剩餘可用的gas,一般來說gasleft()函數被放在函數的開頭和結尾用于判斷整個函數使用了多少gas
// 這裡是為了計算報告送出者送出報告所使用的gas,然後在函數結尾根據gas消耗返還送出者gas費
uint256 initialGas = gasleft(); // This line must come first
// Make sure the transmit message-length matches the inputs. Otherwise, the
// transmitter could append an arbitrarily long (up to gas-block limit)
// string of 0 bytes, which we would reimburse at a rate of 16 gas/byte, but
// which would only cost the transmitter 4 gas/byte. (Appendix G of the
// yellow paper, p. 25, for G_txdatazero and EIP 2028 for G_txdatanonzero.)
// This could amount to reimbursement profit of 36 million gas, given a 3MB
// zero tail.
// 報告長度合理性判斷,包括簽名長度
// 因為在最後返還gas費時有一部分是根據傳入參數長度進行償還,償還價格為16gas/位元組
// 而送出者這部分的實際花費為4gas/位元組,是以為避免送出者惡意套取gas的償還費用需要下列判斷
require(msg.data.length == expectedMsgDataLength(_report, _rs, _ss),
"transmit message too long");
// 定義一個ReportData結構體r
ReportData memory r; // Relieves stack pressure
{
r.hotVars = s_hotVars; // cache read from storage 從s_hotVars直接擷取資料初始化
bytes32 rawObservers; // 定義一個bytes32類型的rawObservers
// 對鍊下報告進行解碼成報告内容,資料提供節點索引和價格資料集合
(r.rawReportContext, rawObservers, r.observations) = abi.decode(
_report, (bytes32, bytes32, int192[])
);
// 鍊下報告内容包括11位元組的0字元填充,16位元組本輪聚合配置參數,4位元組報告所處階段及1位元組報告所處輪次
// rawReportContext consists of:
// 11-byte zero padding
// 16-byte configDigest
// 4-byte epoch
// 1-byte round
// 将報告内容左移88位獲得聚合配置參數(将左邊11位元組的0填充移除以截取配置參數)
bytes16 configDigest = bytes16(r.rawReportContext << 88);
// 判斷報告的配置參數和hotVars的配置參數是否一緻,不一緻則停止上傳報告
require(
r.hotVars.latestConfigDigest == configDigest,
"configDigest mismatch"
);
// 截取報告的階段和輪次資訊
uint40 epochAndRound = uint40(uint256(r.rawReportContext));
// direct numerical comparison works here, because
//
// ((e,r) <= (e',r')) implies (epochAndRound <= epochAndRound')
//
// because alphabetic ordering implies e <= e', and if e = e', then r<=r',
// so e*256+r <= e'*256+r', because r, r' < 256
// 判斷該報告輪次是否是最新為最新輪次
require(r.hotVars.latestEpochAndRound < epochAndRound, "stale report");
// 判斷簽名數量是否大于最大可容忍不誠實節點的數量
require(_rs.length > r.hotVars.threshold, "not enough signatures");
// 判斷簽名數量是否小于最大的預言機節點數量(預言機節點數量在父合約中被定義為31個)
require(_rs.length <= maxNumOracles, "too many signatures");
// 判斷簽名的R,S元件數量是否比對
require(_ss.length == _rs.length, "signatures out of registration");
// 判斷價格資料集的資料數量是否小于最大的預言機節點數量
require(r.observations.length <= maxNumOracles,
"num observations out of bounds");
// 判斷價格資料集的資料數量是否大于2倍的最大可容忍不誠實節點的數量
require(r.observations.length > 2 * r.hotVars.threshold,
"too few values to trust median");
// 擷取簽名集合的V元件集合并傳給r結構體
// Copy signature parities in bytes32 _rawVs to bytes r.v
r.vs = new bytes(_rs.length);
for (uint8 i = 0; i < _rs.length; i++) {
r.vs[i] = _rawVs[i];
}
// 擷取價格資料提供節點的索引資料并傳給r結構體
// Copy observer identities in bytes32 rawObservers to bytes r.observers
r.observers = new bytes(r.observations.length); //有多少價格資料就有多少資料提供節點
bool[maxNumOracles] memory seen; //輔助bool數組,用于判斷有無出現重複的資料提供節點
for (uint8 i = 0; i < r.observations.length; i++) {
uint8 observerIdx = uint8(rawObservers[i]); //擷取提供第i個價格資料的節點索引
require(!seen[observerIdx], "observer index repeated"); //有重複節點則停止報告
seen[observerIdx] = true; //标記某個節點在本輪報告中提供了價格資料
r.observers[i] = rawObservers[i]; //将索引資訊複制到結構體r中
}
Oracle memory transmitter = s_oracles[msg.sender]; //擷取此次報告的送出者
require( // Check that sender is authorized to report
transmitter.role == Role.Transmitter && //檢查該送出者是否有送出報告的權限
msg.sender == s_transmitters[transmitter.index], //檢查送出者的索引是否正确
"unauthorized transmitter"
);
// 擷取此次報告的階段及輪次并傳給r結構體
// record epochAndRound here, so that we don't have to carry the local
// variable in transmit. The change is reverted if something fails later.
r.hotVars.latestEpochAndRound = epochAndRound;
}
{ // Verify signatures attached to report
bytes32 h = keccak256(_report); //加密報告
bool[maxNumOracles] memory signed; //輔助數組,用于判斷是否有重複簽名
Oracle memory o;
for (uint i = 0; i < _rs.length; i++) {
//利用預言機節點各自私鑰對_report簽名後生成的V、S、R元件生成私鑰對應的公鑰(密碼學知識)
address signer = ecrecover(h, uint8(r.vs[i])+27, _rs[i], _ss[i]);
o = s_oracles[signer];
//判斷簽名生成的公鑰位址是否為s_oracles中的簽名者(判斷有無簽名權限)
require(o.role == Role.Signer, "address not authorized to sign");
require(!signed[o.index], "non-unique signature"); //判斷有無重複簽名
signed[o.index] = true; //标記
}
}
{ // Check the report contents, and record the result
// 檢查價格資料集合observations中的價格資料是否已經按照從小到大排序,友善後面取中位數作為結果
for (uint i = 0; i < r.observations.length - 1; i++) {
bool inOrder = r.observations[i] <= r.observations[i+1];
require(inOrder, "observations not sorted");
}
// 取價格資料集合中的中位數作為該輪BTC/ETH價格聚合中的最終結果
int192 median = r.observations[r.observations.length/2];
// 最終價格需要在預設的合理區間内
require(minAnswer <= median && median <= maxAnswer, "median is out of min-max range");
// 2.4.1的requestNewRound函數發出新價格資料請求時沒有變更latestAggregatorRoundId
// 擷取最終價格資料後才将latestAggregatorRoundId變量+1
r.hotVars.latestAggregatorRoundId++;
// 将結果存入s_transmissions映射中
s_transmissions[r.hotVars.latestAggregatorRoundId] =
Transmission(median, uint64(block.timestamp));
//廣播新一輪聚合資料摘要
emit NewTransmission(
r.hotVars.latestAggregatorRoundId,
median,
msg.sender,
r.observations,
r.observers,
r.rawReportContext
);
// Emit these for backwards compatability with offchain consumers
// that only support legacy events
// 廣播新輪次ID
emit NewRound(
r.hotVars.latestAggregatorRoundId,
address(0x0),
block.timestamp
);
// 廣播新價格資料
emit AnswerUpdated(
median,
r.hotVars.latestAggregatorRoundId,
block.timestamp
);
// 資料校驗
validateAnswer(r.hotVars.latestAggregatorRoundId, median);
}
s_hotVars = r.hotVars; //更新s_hotVars
assert(initialGas < maxUint32); //斷言
// 為參與價格聚合的節點發放link代币作為激勵(實際發放過程更複雜一些,在父合約實作)
// 并且償還報告送出者的gas消耗,initialGas在transmit函數開頭通過gasleft()獲得
reimburseAndRewardOracles(uint32(initialGas), r.observers);
}
transmit函數(鍊下報告上鍊函數)
s_transmissions映射和s_hotVars會在transmit函數中被指派,transmit函數就是預言機網絡将鍊下的價格資料報告送出上鍊所要調用的函數,可以了解為預言機網絡和鍊上預言機生态的資料接口。transmit函數的傳入參數為價格資料報告和送出資料的預言機節點的簽名集合,報告中的資料會被解碼提取并存入到s_transmissions映射中,而簽名部分則會被用來做報告合理性的驗證。
由于transmit函數代碼較長,下面先對代碼進行拆分然後再分析。
2.4.3 transmit函數片段1(gasleft和expectedMsgDataLength)
/**
* @notice transmit is called to post a new report to the contract
* @param _report serialized report, which the signatures are signing. See parsing code below for format. The ith element of the observers component must be the index in s_signers of the address for the ith signature
* @param _rs ith element is the R components of the ith signature on report. Must have at most maxNumOracles entries
* @param _ss ith element is the S components of the ith signature on report. Must have at most maxNumOracles entries
* @param _rawVs ith element is the the V component of the ith signature
*/
function transmit(
// NOTE: If these parameters are changed, expectedMsgDataLength and/or
// TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly
// 鍊下報告内容
bytes calldata _report,
// 報告附帶的預言機節點的簽名
bytes32[] calldata _rs, bytes32[] calldata _ss, bytes32 _rawVs // signatures
)
external
{
// 擷取目前剩餘可用的gas,一般來說gasleft()函數被放在函數的開頭和結尾用于判斷整個函數使用了多少gas
// 這裡是為了計算報告送出者送出報告所使用的gas,然後在函數結尾根據gas消耗返還送出者gas費
uint256 initialGas = gasleft(); // This line must come first
// Make sure the transmit message-length matches the inputs. Otherwise, the
// transmitter could append an arbitrarily long (up to gas-block limit)
// string of 0 bytes, which we would reimburse at a rate of 16 gas/byte, but
// which would only cost the transmitter 4 gas/byte. (Appendix G of the
// yellow paper, p. 25, for G_txdatazero and EIP 2028 for G_txdatanonzero.)
// This could amount to reimbursement profit of 36 million gas, given a 3MB
// zero tail.
// 報告長度合理性判斷,包括簽名長度
// 因為在最後返還gas費時有一部分是根據傳入參數長度進行償還,償還價格為16gas/位元組
// 而送出者這部分的實際花費為4gas/位元組,是以為避免送出者惡意套取gas的償還費用需要下列判斷
require(msg.data.length == expectedMsgDataLength(_report, _rs, _ss),
"transmit message too long");
······
}
transmit函數片段1
transmit函數的入參主要分為兩個部分,第一部分是包含價格資料的report,第二部分則是用于簽名驗證的可組成簽名集合的簽名元件集合rs、ss和rawVs,接下來我們分析的重點會側重report這一塊。
函數開頭用gasleft()函數擷取整個函數目前仍可使用的gas,在這裡的目的主要是用于計算報告送出者送出報告所花費的gas數量并以此償還送出者gas消耗(相當于Chainlink官方出發送報告的手續費)。接着函數用require判斷傳入的參數的長度和有效長度是否一緻,因為chainlink是按照傳入的資料長度來進行gas補償(根據報告長度超額補充),是以為了避免有節點發送無效輸入來惡意擷取補償而進行這項判斷。
判斷過程中所使用的函數為expectedMsgDataLength。
function expectedMsgDataLength(
bytes calldata _report, bytes32[] calldata _rs, bytes32[] calldata _ss
) private pure returns (uint256 length)
{
// calldata will never be big enough to make this overflow
return uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) +
_report.length + // one byte pure entry in _report
_rs.length * 32 + // 32 bytes per entry in _rs
_ss.length * 32 + // 32 bytes per entry in _ss
0; // placeholder
}
//TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT定義
// The constant-length components of the msg.data sent to transmit.
// See the "If we wanted to call sam" example on for example reasoning
// https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html
uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT =
4 + // function selector
32 + // word containing start location of abiencoded _report value
32 + // word containing location start of abiencoded _rs value
32 + // word containing start location of abiencoded _ss value
32 + // _rawVs value
32 + // word containing length of _report
32 + // word containing length _rs
32 + // word containing length of _ss
0; // placeholder
expectedMsgDataLength函數實作及相關變量定義
在expectedMsgDataLength函數中可以看到有效輸入的長度實際上就是發起交易時入參為report、rs、ss和rawVs的abi編碼長度(abi編碼為16進制串,包括函數選擇器+變長資料位置和長度辨別+邊長資料内容,這是以太坊底層和solidity的知識點,這裡不過多贅述),目的就是防止交易發起者在msg.data中加入除了上述4個入參之外的其他參數。這裡report中的每個實體長度為1個位元組,簽名rs和ss中的每個實體長度為32個位元組。
2.4.4 transmit函數片段2(ReportData、configDigest和s_oracles)
//定義一個ReportData結構體r
ReportData memory r; // Relieves stack pressure
{
r.hotVars = s_hotVars; // cache read from storage 從s_hotVars直接擷取資料初始化
bytes32 rawObservers; // 定義一個bytes32類型的rawObservers
// 對鍊下報告進行解碼成報告内容,資料提供節點索引和價格資料集合
(r.rawReportContext, rawObservers, r.observations) = abi.decode(
_report, (bytes32, bytes32, int192[])
);
// 鍊下報告内容包括11位元組的0字元填充,16位元組本輪聚合配置參數,4位元組報告所處階段及1位元組報告所處輪次
// rawReportContext consists of:
// 11-byte zero padding
// 16-byte configDigest
// 4-byte epoch
// 1-byte round
// 将報告内容左移88位獲得聚合配置參數(将左邊11位元組的0填充移除以截取配置參數)
bytes16 configDigest = bytes16(r.rawReportContext << 88);
// 判斷報告的配置參數和hotVars的配置參數是否一緻,不一緻則停止上傳報告
require(
r.hotVars.latestConfigDigest == configDigest,
"configDigest mismatch"
);
// 截取報告的階段和輪次資訊
uint40 epochAndRound = uint40(uint256(r.rawReportContext));
// direct numerical comparison works here, because
//
// ((e,r) <= (e',r')) implies (epochAndRound <= epochAndRound')
//
// because alphabetic ordering implies e <= e', and if e = e', then r<=r',
// so e*256+r <= e'*256+r', because r, r' < 256
// 判斷該報告輪次是否是最新為最新輪次
require(r.hotVars.latestEpochAndRound < epochAndRound, "stale report");
// 判斷簽名數量是否大于最大可容忍不誠實節點的數量
require(_rs.length > r.hotVars.threshold, "not enough signatures");
// 判斷簽名數量是否小于最大的預言機節點數量(預言機節點數量在父合約中被定義為31個)
require(_rs.length <= maxNumOracles, "too many signatures");
// 判斷簽名的R,S元件數量是否比對
require(_ss.length == _rs.length, "signatures out of registration");
// 判斷價格資料集的資料數量是否小于最大的預言機節點數量
require(r.observations.length <= maxNumOracles,
"num observations out of bounds");
// 判斷價格資料集的資料數量是否大于2倍的最大可容忍不誠實節點的數量
require(r.observations.length > 2 * r.hotVars.threshold,
"too few values to trust median");
// 擷取簽名集合的V元件集合并傳給r結構體
// Copy signature parities in bytes32 _rawVs to bytes r.v
r.vs = new bytes(_rs.length);
for (uint8 i = 0; i < _rs.length; i++) {
r.vs[i] = _rawVs[i];
}
// 擷取價格資料提供節點的索引資料并傳給r結構體
// Copy observer identities in bytes32 rawObservers to bytes r.observers
r.observers = new bytes(r.observations.length); //有多少價格資料就有多少資料提供節點
bool[maxNumOracles] memory seen; //輔助bool數組,用于判斷有無出現重複的資料提供節點
for (uint8 i = 0; i < r.observations.length; i++) {
uint8 observerIdx = uint8(rawObservers[i]); //擷取提供第i個價格資料的節點索引
require(!seen[observerIdx], "observer index repeated"); //有重複節點則停止報告
seen[observerIdx] = true; //标記某個節點在本輪報告中提供了價格資料
r.observers[i] = rawObservers[i]; //将索引資訊複制到結構體r中
}
Oracle memory transmitter = s_oracles[msg.sender]; //擷取此次報告的送出者
require( // Check that sender is authorized to report
transmitter.role == Role.Transmitter && //檢查該送出者是否有送出報告的權限
msg.sender == s_transmitters[transmitter.index], //檢查送出者的索引是否正确
"unauthorized transmitter"
);
// 擷取此次報告的階段及輪次并傳給r結構體
// record epochAndRound here, so that we don't have to carry the local
// variable in transmit. The change is reverted if something fails later.
r.hotVars.latestEpochAndRound = epochAndRound;
}
transmit函數片段2
由于第2個片段較長,是以每一行代碼的作用我在注釋中給出,下面隻對一些關鍵部分展開描述。transmit定義了ReportData結構體的執行個體化r用于接收報告内容。
//ReportData定義
// Used to relieve stack pressure in transmit
struct ReportData {
//最新輪次報告資料
HotVars hotVars; // Only read from storage once
//價格資料提供節點集合的索引資料
bytes observers; // ith element is the index of the ith observer
//價格資料集合
int192[] observations; // ith element is the ith observation
//簽名集合v元件
bytes vs; // jth element is the v component of the jth signature
//鍊下報告内容
bytes32 rawReportContext;
}
ReportData定義
r中hotVars通過擷取上一輪的s_hotVars進行初始化,rawReportContext和observations則是将入參_report進行解碼後獲得。rawReportContext為原生的鍊下報告内容,主要包括報告配置configDigest和階段輪次資料epochAndRound,階段輪次資料這裡不再過多說明,報告配置資料則主要是由預言機節點資訊,最大可容忍不誠實節點數量,版本資訊等資料加密後獲得,用于判斷報告中所使用的預言機網絡節點配置是否符合提前設定的需求。
function configDigestFromConfigData(
address _contractAddress, //合約位址
uint64 _configCount, //配置版本号,每次配置變更+1
address[] calldata _signers, //簽名節點位址集合
address[] calldata _transmitters, //報告送出節點位址集合
uint8 _threshold, //最大可容忍誠實節點數量
uint64 _encodedConfigVersion, //鍊下編碼版本号
bytes calldata _encodedConfig //鍊下編碼配置
) internal pure returns (bytes16) {
return bytes16(keccak256(abi.encode(_contractAddress, _configCount,
_signers, _transmitters, _threshold, _encodedConfigVersion, _encodedConfig
)));
}
configDigest生成函數configDigestFromConfigData
從報告配置參數configDigest的生成函數中可以看到該參數是對合約位址、配置版本号、簽名節點和報告送出節點位址集合等資訊進行keccak256編碼後獲得的,這也就意味着一旦報告中節點或者位址等資料發送變動,該報告就無法通過require對configDigest的判斷,以此降低報告的錯誤率和增加報告的抗碰撞性。
transmit函數片段2的中間部分主要是對一些資料和簽名的合理性進行判斷,這一部分涉及到數字簽名及拜占庭容錯等知識,感興趣的可以在Chainlink白皮書或者上網自行查閱資料了解,這裡不做贅述。我們還可以看到合約中定義了一個最大的預言機節點數量maxNumOracles,這個變量在該合約的父合約OffchainAggregatorBilling中被定義為31。
// Maximum number of oracles the offchain reporting protocol is designed for
uint256 constant internal maxNumOracles = 31;
OffchainAggregatorBilling父合約中maxNumOracles的定義
函數片段2的最後對價格資料提供節點索引rawObservers和報告送出者transmitter進行了處理,先分析rawObservers。rawObservers從_report中直接解碼獲得,其代表了所有提供價格資料的節點的索引,比如rawObservers的第2位是3,那麼它就代表observations價格集合中的第2個價格資料是由索引為3的預言機節點提供。Chainlink不允許一個節點在一份報告中提供兩個資料,是以設定了bool[maxNumOracles]數組用于輔助判斷,當發現observations集合中有兩個資料都由同一個索引提供時,則會駁回這次報告。
接着分析報告送出者transmitter。transmit函數需要記錄此次報告的送出者以友善發放送出獎勵,并且為了達到這一目的合約中還需要有從鍊下預言機節點到鍊上送出位址的映射來确認是哪個預言機對應的位址獲得該獎勵進而友善後繼的獎勵發放和資料統計。聚合器合約使用s_oracles映射和s_transmitters數組來實作上述功能,這兩個資料結構被定義在OffchainAggregator合約的父合約OffchainAggregatorBilling中。
mapping (address=> Oracle) internal s_oracles;
struct Oracle {
// 預言機節點索引号
uint8 index; // Index of oracle in s_signers/s_transmitters
// 在報告中扮演的角色
Role role; // Role of the address which mapped to this struct
}
// Used for s_oracles[a].role, where a is an address, to track the purpose
// of the address, or to indicate that the address is unset.
enum Role {
// No oracle role has been set for address a
Unset, // 沒有角色
// Signing address for the s_oracles[a].index'th oracle. I.e., report
// signatures from this oracle should ecrecover back to address a.
Signer, // 簽名者
// Transmission address for the s_oracles[a].index'th oracle. I.e., if a
// report is received by OffchainAggregator.transmit in which msg.sender is
// a, it is attributed to the s_oracles[a].index'th oracle.
Transmitter // 送出者
}
OffchainAggregatorBilling父合約中s_oracles的相關定義
s_oracles是從address到Oracle結構體的映射,而Oracle結構體中包含用于辨別預言機的索引号和該預言機在某次報告中所扮演的角色Role。Role是枚舉類型包含三種角色,包括Unset(未配置設定角色)、Signer(報告簽名者)和Transmitter(報告送出者)。從s_oracles的定義中可以看出,我們可以通過address找到該位址所對應的預言機索引,并且檢視該預言機在此次報告中所扮演的角色,進而根據所扮演的角色執行特定操作。
// s_transmitters contains the transmission address of each oracle,
// i.e. the address the oracle actually sends transactions to the contract from
address[] internal s_transmitters;
OffchainAggregatorBilling父合約中s_transmitters的定義
s_transmitters是一個位址數組,存放的是每個預言機節點将鍊下報告發送至聚合器合約所使用的位址,其數組索引對應的是s_oracles中Oracle結構體的index。
了解完s_oracles和s_transmitters的定義後再來看transmit函數片段2最後一段代碼就能發現這段代碼實際上是在判斷此次報告的送出者是否為目前輪次被預言機網絡選出的送出者,是否是登記在s_transmitters中的預言機位址,以此來判斷鍊下報告的有效性。
2.4.5 transmit函數片段3(擷取最終價格資料median)
{ // Verify signatures attached to report
bytes32 h = keccak256(_report); //加密報告
bool[maxNumOracles] memory signed; //輔助數組,用于判斷是否有重複簽名
Oracle memory o;
for (uint i = 0; i < _rs.length; i++) {
//利用預言機節點各自私鑰對_report簽名後生成的V、S、R元件生成私鑰對應的公鑰(密碼學知識)
address signer = ecrecover(h, uint8(r.vs[i])+27, _rs[i], _ss[i]);
o = s_oracles[signer];
//判斷簽名生成的公鑰位址是否為s_oracles中的簽名者(判斷有無簽名權限)
require(o.role == Role.Signer, "address not authorized to sign");
require(!signed[o.index], "non-unique signature"); //判斷有無重複簽名
signed[o.index] = true; //标記
}
}
{ // Check the report contents, and record the result
// 檢查價格資料集合observations中的價格資料是否已經按照從小到大排序,友善後面取中位數作為結果
for (uint i = 0; i < r.observations.length - 1; i++) {
bool inOrder = r.observations[i] <= r.observations[i+1];
require(inOrder, "observations not sorted");
}
// 取價格資料集合中的中位數作為該輪BTC/ETH價格聚合中的最終結果
int192 median = r.observations[r.observations.length/2];
// 最終價格需要在預設的合理區間内
require(minAnswer <= median && median <= maxAnswer, "median is out of min-max range");
// 2.4.1的requestNewRound函數發出新價格資料請求時沒有變更latestAggregatorRoundId
// 擷取最終價格資料後才将latestAggregatorRoundId變量+1
r.hotVars.latestAggregatorRoundId++;
// 将結果存入s_transmissions映射中
s_transmissions[r.hotVars.latestAggregatorRoundId] =
Transmission(median, uint64(block.timestamp));
//廣播新一輪聚合資料摘要
emit NewTransmission(
r.hotVars.latestAggregatorRoundId,
median,
msg.sender,
r.observations,
r.observers,
r.rawReportContext
);
// Emit these for backwards compatability with offchain consumers
// that only support legacy events
// 廣播新輪次ID
emit NewRound(
r.hotVars.latestAggregatorRoundId,
address(0x0),
block.timestamp
);
// 廣播新價格資料
emit AnswerUpdated(
median,
r.hotVars.latestAggregatorRoundId,
block.timestamp
);
// 資料校驗
validateAnswer(r.hotVars.latestAggregatorRoundId, median);
}
s_hotVars = r.hotVars; //更新s_hotVars
assert(initialGas < maxUint32); //斷言
// 為參與價格聚合的節點發放link代币作為激勵(實際發放過程更複雜一些,在父合約實作)
// 并且償還報告送出者的gas消耗,initialGas在transmit函數開頭通過gasleft()獲得
reimburseAndRewardOracles(uint32(initialGas), r.observers);
transmit函數片段3
函數片段3的代碼功能已經在注釋中給出,開頭部分是在驗證報告裡簽名的合理性,驗證簽名者的公鑰是否為s_oracles裡登記的signer或transmitter,這裡用到ecrecover函數來生成公鑰,大概思路就是“用私鑰簽名的報告簽名”+“報告”=“私鑰對應的公鑰”,其中報告簽名可以由R、S、V三個元件組成。
片段3的第二部分主要是從價格資料集合observations中擷取最終的BTC/ETH價格資料,而擷取的方法則是直接從集合中取中位數。報告送出節點在收集到其它節點送出的價格資料後會先将資料從小到大排序再打包發送至聚合器合約,然後聚合器合約就可以直接在集合的中間位置(observations.length/2)擷取到最終價格。取中位數可以避免價格集合中最大值或最小值偏差過大所帶來的影響,比取平均值有更強的穩定性。(實際上個人認為是因為chainlink網絡中節點的規模相對較小,單純取平均值的話最值的影響相對較大,當然每個節點送出的資料本身就是多資料源聚合後所得這一點也保證了取中位數的有較高的可靠性)
取到的最終價格median會被儲存在s_transmissions中供價格參考合約擷取,值得一提的是當擷取到最終價格後,聚合器合約才對hotVars的latestAggregatorRoundId變量進行更新,以此避免roundId更新但擷取不到對應價格的情況。而擷取到最終價格并且對latestAggregatorRoundId更新後,transmit函數會将這一輪聚合的結果進行廣播。
廣播完後transmit函數還用validateAnswer對最終價格進行校驗,這裡的校驗主要是将最終價格和上一輪價格作為入參進行比對,但是即使出現異常情況validateAnswer也不會阻止該報告上鍊,僅會将異常抛出。資料校驗功能由專門的validater合約實作,不過目前已上鍊的聚合器合約中validateAnswer函數内validater合約位址被置為0位址,是以該函數實際上不起作用,這裡不過多解析。
當transmit函數完成對鍊下報告的處理并将價格資料存入合約後,它會調用reimburseAndRewardOracles函數對參與此輪價格聚合的節點發放link代币獎勵,并且報帳報告送出節點送出報告所花費的手續費。reimburseAndRewardOracles函數由OffchainAggregatorBilling合約實作。
2.5 OffchainAggregatorBilling合約
OffchainAggregatorBilling合約負責向參與PriceFeeds的預言機節點發放代币,包括link激勵和gas償還。這裡主要分析一下和reimburseAndRewardOracles函數相關的部分代碼。
function reimburseAndRewardOracles(
uint32 initialGas, //發送鍊下報告時最開始記錄下的剩餘可用gas,用于計算發送鍊下報告的總gas開銷
bytes memory observers //發送價格資料集合的所有節點索引,每一位代表一個節點的索引
)
internal
{
Oracle memory txOracle = s_oracles[msg.sender]; //記錄送出報告的預言機節點
Billing memory billing = s_billing; //賬單相關參數,記錄固定激勵金額以及每機關gas報帳額度等
// Reward oracles for providing observations. Oracles are not rewarded
// for providing signatures, because signing is essentially free.
// 獲得預言機送出價格資料的次數以用于發放link代币獎勵
// link代币不會在送出資料後立即發放,而是可以在多次送出價格資料後由預言機主動領取(節約gas)
// oracleRewards函數會對本輪送出過資料的節點的送出資料次數進行更新,以友善後續獎勵發放
s_oracleObservationsCounts =
oracleRewards(observers, s_oracleObservationsCounts);
// Reimburse transmitter of the report for gas usage
require(txOracle.role == Role.Transmitter, //報告需要由本輪被標明出的transmitter送出
"sent by undesignated transmitter"
);
uint256 gasPrice = impliedGasPrice( //設定合理的gasPrice
tx.gasprice / (1 gwei), // convert to ETH-gwei units
billing.reasonableGasPrice,
billing.maximumGasPrice
);
// 計算callData的gas開銷
// The following is only an upper bound, as it ignores the cheaper cost for
// 0 bytes. Safe from overflow, because calldata just isn't that long.
uint256 callDataGasCost = 16 * msg.data.length;
// If any changes are made to subsequent calculations, accountingGasCost
// needs to change, too.
uint256 gasLeft = gasleft(); //擷取目前交易剩餘可用gas
uint256 gasCostEthWei = transmitterGasCostEthWei(// 擷取送出該報告所花費的gas總額
uint256(initialGas),
gasPrice,
callDataGasCost,
gasLeft
);
// microLinkPerEth is 1e-6LINK/ETH units, gasCostEthWei is 1e-18ETH units
// (ETH-wei), product is 1e-24LINK-wei units, dividing by 1e6 gives
// 1e-18LINK units, i.e. LINK-wei units
// Safe from over/underflow, since all components are non-negative,
// gasCostEthWei will always fit into uint128 and microLinkPerEth is a
// uint32 (128+32 < 256!).
// 計算所需補償的link代币
uint256 gasCostLinkWei = (gasCostEthWei * billing.microLinkPerEth)/ 1e6;
// Safe from overflow, because gasCostLinkWei < 2**160 and
// billing.linkGweiPerTransmission * (1 gwei) < 2**64 and we increment
// s_gasReimbursementsLinkWei[txOracle.index] at most 2**40 times.
// 計算需要發送給該報告送出者的總link代币數量(gas報帳+發送報告獎勵)
s_gasReimbursementsLinkWei[txOracle.index] =
s_gasReimbursementsLinkWei[txOracle.index] + gasCostLinkWei +
uint256(billing.linkGweiPerTransmission) * (1 gwei); // convert from linkGwei to linkWei
// Uncomment next line to compute the remaining gas cost after above gasleft().
// See OffchainAggregatorBilling.accountingGasCost docstring for more information.
//
// gasUsedInAccounting = gasLeft - gasleft();
}
reimburseAndRewardOracles函數
reimburseAndRewardOracles函數實際上可以看作是一個獎勵發放函數,用于向PriceFeeds參與節點發放link代币獎勵。需要注意的是,reimburseAndRewardOracles不直接向節點轉賬,而僅是做一個記錄,每當一個報告被送出時,reimburseAndRewardOracles會更新價格資料送出者送出的次數和報告送出者的報帳和獎勵數額,然後節點可以根據記錄領取獎勵。這樣設計的好處是不需要頻繁地進行轉賬操作,預言機節點可以一次性領取多次價格聚合的獎勵,進而降低gas開銷。
reimburseAndRewardOracles函數主要就做了三件事,一是擷取報告送出節點的相關資訊txOracle和賬單設定billing;二是用s_oracleObservationsCounts記錄節點送出價格資料的次數,作為發放送出價格資料獎勵的依據;三是用s_gasReimbursementsLinkWei記錄節點的送出報告開銷和送出報告獎勵,作為發放送出報告獎勵的依據。
下面也會根據這三件事展開分析。
2.5.1 txOracle和billing
txOracle實際上就是通過報告送出者的位址擷取到該節點對應的s_oracles對象,s_oracles相關的部分在2.4.4節中有提及這裡不做重複。
billing則是Billing結構體的執行個體化對象s_billing,裡面主要記錄發放獎勵時所需要設定的參數,如固定激勵金額以及每機關gas報帳等。
// Parameters for oracle payments
struct Billing {
// Highest compensated gas price, in ETH-gwei uints
uint32 maximumGasPrice; // 發送報告的最大可接受gasPrice
// If gas price is less (in ETH-gwei units), transmitter gets half the savings
// 合理的gasPrice,如果報告送出者transmitter送出報告時的gasPrice低于reasonableGasPrice
// 則報告送出者可以獲得比實際gasPrice更多的gasPrice補償
uint32 reasonableGasPrice;
// Pay transmitter back this much LINK per unit eth spent on gas
// (1e-6LINK/ETH units)
uint32 microLinkPerEth; //根據花費在gas的每機關eth開銷而返還給報告送出者的link代币
// Fixed LINK reward for each observer, in LINK-gwei units
uint32 linkGweiPerObservation; //送出一次價格資料的固定link代币獎勵
// Fixed reward for transmitter, in linkGweiPerObservation units
uint32 linkGweiPerTransmission; //送出一次報告的固定link代币獎勵
}
Billing internal s_billing;
Billing結構體定義
Billing結構體中字段的函數如上面注釋所示,上述字段在OffchainAggregatorBilling合約建構的時候會進行初始化,并且可以通過setBilling函數進行設定(該函數這裡不做說明)。
2.5.2 s_oracleObservationsCounts和oracleRewards
s_oracleObservationsCounts是一個用于記錄每個預言機節點送出價格資料次數的數組,其定義如下:
// ith element is number of observation rewards due to ith process, plus one.
// This is expected to saturate after an oracle has submitted 65,535
// observations, or about 65535/(3*24*20) = 45 days, given a transmission
// every 3 minutes.
//
// This is always one greater than the actual value, so that when the value is
// reset to zero, we don't end up with a zero value in storage (which would
// result in a higher gas cost, the next time the value is incremented.)
// Calculations using this variable need to take that offset into account.
uint16[maxNumOracles] internal s_oracleObsrvationsCounts;
s_oracleObservationsCounts定義
s_oracleObservationsCounts是個uint16的數組,長度為最大的預言機數量maxNumOracles,也就是31。其數組下标對應各個預言機節點的索引Oracle.index,而下标對應的值就是該預言機在過去一段時間已送出且尚未領取獎勵的價格資料的個數(一次聚合中一個節點隻能送出一個價格資料),當該節點領取完獎勵後該值會被置為1(不置為0是為了節約gas開銷,以太坊虛拟機中将狀态從零值變為非零值會有額外的gas開銷)。
reimburseAndRewardOracles函數中通過調用oracleRewards函數對s_oracleObservationsCounts數組進行更新。
function oracleRewards(
bytes memory observers,
uint16[maxNumOracles] memory observations
)
internal
pure
returns (uint16[maxNumOracles] memory)
{
// reward each observer-participant with the observer reward
for (uint obsIdx = 0; obsIdx < observers.length; obsIdx++) {
uint8 observer = uint8(observers[obsIdx]); //擷取預言機節點下标index
// observer對應下标的預言機節點的價格資料送出個數+1,為防止溢出進行了特殊處理
observations[observer] = saturatingAddUint16(observations[observer], 1);
}
return observations;
}
oracleRewards函數定義
oracleRewards函數的傳入參數是observers和observations,其中observers對應的是2.4.4中提到的rawObservers或r.observers,observations對應的則是s_oracleObservationsCounts(特别注意,這裡的observations不是2.4節transmit函數裡的observations)。
從上面的函數定義可以看出oracleRewards函數的作用就是通過傳入的observers獲得該輪參與聚合的節點下标,然後根據下标将s_oracleObservationsCounts中所有參與節點對應的資料送出個數+1。為了防止資料送出個數溢出(超過65536),oracleRewards函數還調用了saturatingAddUint16對每次的累加進行處理。
function saturatingAddUint16(uint16 _x, uint16 _y)
internal
pure
returns (uint16)
{
return uint16(min(uint256(_x)+uint256(_y), maxUint16));
}
saturatingAddUint16函數定義
saturatingAddUint16函數的實作比較簡單,就是傳回入參x,y之和以及65536之間的較小值。也就是說當某個預言機送出過65536個資料且沒有領取獎勵後,即使再送出新的資料其資料送出個數也不會再增加,依舊為65536。
簡而言之,節點可以根據s_oracleObservationsCounts内的記錄領取價格資料送出的獎勵,而每次送出報告時報告送出者都會通過oracleRewards函數對s_oracleObservationsCounts内的資料進行更新。
2.5.3 gas報帳計算和s_gasReimbursementsLinkWei
在2.5開頭我們可以看到,reimburseAndRewardOracles函數在對s_oracleObservationsCounts進行更新後的後續代碼主要就是在計算transmitter送出報告時的gas開銷,以友善對transmitter進行gas報帳和報告送出獎勵。
首先第一步是用impliedGasPrice函數計算一個用于gas報帳的gasPrice。
// Gas price at which the transmitter should be reimbursed, in ETH-gwei/gas
function impliedGasPrice(
uint256 txGasPrice, // ETH-gwei/gas units //實際的gasPrice
uint256 reasonableGasPrice, // ETH-gwei/gas units //官方設定的合理的gasPrice
uint256 maximumGasPrice // ETH-gwei/gas units //可接受的最大gasPrice
)
internal
pure
returns (uint256)
{
// Reward the transmitter for choosing an efficient gas price: if they manage
// to come in lower than considered reasonable, give them half the savings.
//
// The following calculations are all in units of gwei/gas, i.e. 1e-9ETH/gas
// chainlink鼓勵報告送出者選擇低gasPrice,并會對該行為進行獎勵
uint256 gasPrice = txGasPrice; //擷取實際的gasPrice
if (txGasPrice < reasonableGasPrice) { //如果送出報告時的gasPrice較低則給予獎勵
// Give transmitter half the savings for coming in under the reasonable gas price
gasPrice += (reasonableGasPrice - txGasPrice) / 2;//多獎勵兩者差額的一半
}
// Don't reimburse a gas price higher than maximumGasPrice
// 如果送出報告時gasPrice過高,隻會報帳低于maximumGasPrice的部分
return min(gasPrice, maximumGasPrice); //傳回兩者之間較小值
}
impliedGasPrice函數定義
第二步計算傳入參數的gas開銷callDataGasCost,這一步比較簡單,chainlink對傳入的報告每個位元組報帳16gas(實際開銷為4gas/位元組)。第三步是用transmitterGasCostEthWei函數計算運作整個transmit函數所需的gas開銷。
// gas reimbursement due the transmitter, in ETH-wei
//
// If this function is changed, accountingGasCost needs to change, too. See
// its docstring
function transmitterGasCostEthWei(
uint256 initialGas, // transmit函數開頭記錄的剩餘可用gas
uint256 gasPrice, // ETH-gwei/gas units //用impliedGasPrice函數算出的gasPrice
uint256 callDataCost, // gas units //傳入參數calldata的gas開銷
uint256 gasLeft // 調用transmitterGasCostEthWei函數前剩餘可用gas
)
internal
pure
returns (uint128 gasCostEthWei)
{
require(initialGas >= gasLeft, "gasLeft cannot exceed initialGas");
uint256 gasUsed = // gas units //計算總共使用的gas
initialGas - gasLeft + // observed gas usage //transmit函數開始到該函數前的總gas開銷
// accountingGasCost為reimburseAndRewardOracles函數後續語句的gas開銷
// 因為調用transmitterGasCostEthWei函數後還會執行其它語句,是以要計算accountingGasCost
callDataCost + accountingGasCost; // estimated gas usage
// gasUsed is in gas units, gasPrice is in ETH-gwei/gas units; convert to ETH-wei
uint256 fullGasCostEthWei = gasUsed * gasPrice * (1 gwei); //計算總gas開銷
assert(fullGasCostEthWei < maxUint128); // the entire ETH supply fits in a uint128...
return uint128(fullGasCostEthWei); //傳回總gas開銷
}
transmitterGasCostEthWei函數定義
transmitterGasCostEthWei函數的語句作用如上所示,從函數可以看到發送報告所使用的總gas數量gasUsed由兩部分組成,第一部分是傳入calldata所需要的gas數量,第二部分是整個transmit函數運作時所需要的gas數量。而第二部分在transmitterGasCostEthWei函數中又被分為transmitterGasCostEthWei函數前使用gas數量(initialGas - gasLeft)和transmitterGasCostEthWei函數後使用gas數量accountingGasCost。
獲得gasUsed後就可以用impliedGasPrice函數得到的gasPrice來計算最後的總gas開銷gasCostEthWei。
// This value needs to change if maxNumOracles is increased, or the accounting
// calculations at the bottom of reimburseAndRewardOracles change.
//
// To recalculate it, run the profiler as described in
// ../../profile/README.md, and add up the gas-usage values reported for the
// lines in reimburseAndRewardOracles following the "gasLeft = gasleft()"
// line. E.g., you will see output like this:
//
// 7 uint256 gasLeft = gasleft();
// 29 uint256 gasCostEthWei = transmitterGasCostEthWei(
// 9 uint256(initialGas),
// 3 gasPrice,
// 3 callDataGasCost,
// 3 gasLeft
// .
// .
// .
// 59 uint256 gasCostLinkWei = (gasCostEthWei * billing.microLinkPerEth)/ 1e6;
// .
// .
// .
// 5047 s_gasReimbursementsLinkWei[txOracle.index] =
// 856 s_gasReimbursementsLinkWei[txOracle.index] + gasCostLinkWei +
// 26 uint256(billing.linkGweiPerTransmission) * (1 gwei);
//
// If those were the only lines to be accounted for, you would add up
// 29+9+3+3+3+59+5047+856+26=6035.
uint256 internal constant accountingGasCost = 6035;
accountingGasCost定義和計算
逐句計算最後部分所示用的gas數量後得到的accountingGasCost為6035,後續如果chainlink對調用transmitterGasCostEthWei函數後的語句進行更改,則accountingGasCost也要重新計算并更改。
計算完總gas開銷gasCostEthWei後,第四步就是根據gasCostEthWei計算需要報帳給tranmitter的link代币(chainlink用link代币來結算報帳和獎勵)。每eth的gas開銷所補償的link代币數量被定義在Billing結構體的microLinkPerEth字段中,最後算出來的總link代币報帳數量為gasCostLinkWei。
第五步,也就是最後一步則是計算總共需要發送給transmitter的總link代币數量(發送報告本身也有link代币獎勵),然後累加進s_gasReimbursementsLinkWei數組中。總link代币發放數量為gasCostLinkWei加上送出報告獎勵billing.linkGweiPerTransmission。s_gasReimbursementsLinkWei和s_oracleObservationsCounts類似,多次累加,一次性領取,初始值為1。
2.5.4 節點領取link代币獎勵(payOracle和owedPayment)
從2.5.1到2.5.3這三小節可以看出,reimburseAndRewardOracles實際上就是對節點應該領取的獎勵和應該返還給報告送出節點的報帳進行記錄,但還沒有真正得将獎勵發放下去。節點可以通過withdrawPayment函數提取link代币。
節點領取的link代币分為兩部分,一部分是提供價格資料的獎勵,被記錄在s_oracleObservationsCounts中,另一部分是節點作為transmitter時的gas報帳及送出報告獎勵,被記錄在s_gasReimbursementsLinkWei中。
function withdrawPayment(address _transmitter)
external
{
// 需要發起link代币提現的位址為預先登記在案的收款位址
require(msg.sender == s_payees[_transmitter], "Only payee can withdraw");
payOracle(_transmitter); //往收款位址支付link代币
}
withdrawPayment函數實作
調用withdrawPayment函數需要傳入預言機節點的transmitter位址,然後withdrawPayment函數會判斷該位址對應的收款位址是否為交易發起位址(隻有收款位址才能調用該函數),最後該函數會調用payOracle向收款位址支付link代币。
// Addresses at which oracles want to receive payments, by transmitter address
mapping (address /* transmitter */ => address /* payment address */)
internal
s_payees;
s_payees定義
s_payees是從節點位址到收款位址的映射,節點送出報告的位址和收款位址可以不同。
// payOracle pays out _transmitter's balance to the corresponding payee, and zeros it out
function payOracle(address _transmitter)
internal
{
Oracle memory oracle = s_oracles[_transmitter]; //擷取Oracle資訊
// 用owedPayment函數擷取該預言機節點可擷取的link代币數量
uint256 linkWeiAmount = owedPayment(_transmitter);
if (linkWeiAmount > 0) {
address payee = s_payees[_transmitter]; //擷取該預言機收款位址
// Poses no re-entrancy issues, because LINK.transfer does not yield
// control flow.
// 調用link代币合約向收款位址轉賬
require(LINK.transfer(payee, linkWeiAmount), "insufficient funds");
// 初始化s_oracleObservationsCounts中對應預言機的送出資料數量
s_oracleObservationsCounts[oracle.index] = 1; // "zero" the counts. see var's docstring
// 初始化s_gasReimbursementsLinkWei中對應預言機的可擷取報帳和獎勵的link代币數量
s_gasReimbursementsLinkWei[oracle.index] = 1; // "zero" the counts. see var's docstring
emit OraclePaid(_transmitter, payee, linkWeiAmount); //廣播
}
}
payOracle函數實作
payOracle函數通過owedPayment擷取transmitter對應預言機可提取的link代币總量linkWeiAmount,然後從s_payees映射擷取收款位址并轉賬,轉完帳後将該預言機對應的剩餘未領取獎勵記錄進行初始化,最後再對提現結果進行廣播。
/**
* @notice query an oracle's payment amount
* @param _transmitter the transmitter address of the oracle
*/
function owedPayment(address _transmitter)
public
view
returns (uint256)
{
Oracle memory oracle = s_oracles[_transmitter]; //擷取Oracle資訊
if (oracle.role == Role.Unset) { return 0; } //預言機需要已注冊在案
Billing memory billing = s_billing; //擷取billing賬單設定
// 擷取資料送出獎勵,= 資料送出個數 * 每個資料的link獎勵數量
uint256 linkWeiAmount =
uint256(s_oracleObservationsCounts[oracle.index] - 1) * //初始值為1,需減去
uint256(billing.linkGweiPerObservation) *
(1 gwei);
// 加上該預言機作為transmitter送出報告時的報帳和獎勵
linkWeiAmount += s_gasReimbursementsLinkWei[oracle.index] - 1;
return linkWeiAmount; // = 資料送出獎勵 + 報告送出獎勵 + gas報帳
}
owedPayment函數實作
owedPayment函數的作用是擷取某個預言機目前可領取的link代币數量。目前可領取代币數量=送出資料個數*送出每個資料的獎勵+報告送出獎勵和gas報帳。
2.6 PriceFeeds合約部署時constrctor賦予的初始值
PriceFeeds采用的是多合約檔案集中部署的形式,是以2.1到2.5提到的所有合約中BTC/ETH價格參考合約EACAggregatorProxy及其父合約AggregatorProxy被部署在同一合約位址下,它們依賴的價格資料來源聚合器合約AccessControlledOffchainAggregator和其父合約OffchainAggregator、OffchainAggregatorBilling被部署在同一合約位址下。
為了加深對于合約中定義的各種狀态變量的了解,下面從etherscan中分别查詢兩個合約位址部署時構造函數為某些變量賦予的初始值(是函數部署時賦予的值,與目前可能不同)。\
價格參考合約constrctor賦予的初始值
聚合器合約constrctor賦予的初始值
3 PriceFeeds合約調用示意圖
PriceFeeds合約調用示意圖
4 最後
攥寫這篇文章的目的主要是加深自己對于Chainlink PriceFeeds合約的了解,并作為記錄以友善後續回頭查閱。為了降低閱讀門檻會有些許地方稍顯啰嗦,各位挑選可能對自己有用處的地方來看即可(整篇文章是照着我看代碼的順序梳理的,是以按順序看可能更容易了解)。
合約分析中隻講了我覺得有必要講的地方,并沒有列出所有的函數及變量。這篇文章可以作為入門,但是真正要深入了解PriceFeeds的鍊上部分還是得去看白皮書和源碼。
文章攥寫的時間跨度較長,内容也較多,并且代碼解析部分也是基于我的了解寫的,是以肯定會有了解錯誤的地方,歡迎提出錯誤,我會以最快的速度修改。
有疑惑的地方也可以提出來大家一起讨論。