In the blockchain field, an oracle is a system that can provide external information to smart contracts on the chain. As the middleware connecting smart contracts and the world outside the blockchain, the oracle plays an extremely critical infrastructure role. Its main function is to provide data for smart contracts in the blockchain.
For example, if we create a smart contract on the Ethereum network, and this contract needs to access the trading volume data of crude oil on a certain day. However, the smart contract itself cannot obtain this kind of real-world data off-chain, so it needs to be implemented through an oracle. In this case, the smart contract will write the crude oil trading volume on the required date into the event log, and then a process will be started off-chain to monitor and subscribe to this event log. When a request in a transaction is monitored, the process will upload the crude oil trading volume information on the specified date to the smart contract by submitting an on-chain transaction and calling the relevant method of the contract.
Data from https://defillama.com/oracles
Chainlink
In the blockchain, the Chainlink oracle has the largest market share. Chainlink is a decentralized oracle project that provides real-world data to the blockchain in the most secure way. Chainlink has established a virtuous cycle ecosystem around the LINK token through economic incentives based on the implementation of the basic oracle principle. The Chainlink oracle needs to be triggered by the transfer of LINK tokens. LINK is an ERC677 contract on the Ethereum network. The oracle function based on the LINK ERC677 token belongs to the request/response mode.
transferAndCall in ERC677 tokens
The oracle is essentially the party that provides the service. When ChainLink designed the oracle framework, the first thing it thought of was how the oracle users could pay the oracle for the service. However, since the standard homogeneous token contract ERC20 cannot meet the requirement of providing services after payment, ChainLink proposed a standard suitable for oracle service scenarios - ERC677.
As can be seen from the above code, ERC677 actually just adds a transferAndCall method based on the standard ERC20. This method combines payment and service request into one, meeting the needs of oracle business scenarios.
When a user performs a transferAndCall, in addition to ERC20 transfers, it will also determine whether the to address is a contract address. If so, the onTokenTransfer method of the to address will be called. (Here, ERC677Receiver has only one method: onTokenTransfer)
We can also go to Etherscan to view the contract source code of the LINK token: https://etherscan.io/address/0x514910771af9ca656af840dff83e8264ecf986ca#code
It can be seen that LINK Token actually inherits the transferAndCall method of ERC677 except for the verification of the _to address when it is implemented. Note: Before requesting the oracle service, you must first determine whether the oracle is trustworthy, because the oracle needs to be paid before providing services to consumers. (Everyone can provide oracle services)
Oracle credibility classification
On-chain oracle request
Let's take a look at how the onTokenTransfer method of the oracle contract is implemented:
When the oracle consumer uses the transferAndCall method to pay the fee and request the oracle's service, the to address here is the address of the requested oracle. The onTokenTransfer method in the oracle will first verify whether the transfer is a LINK token (onlyLINK), which is actually to determine whether msg.sender is the address of the Link token contract. Then it will determine whether the length of _data exceeds the maximum limit. Finally, it will determine whether there is a function selector starting with "oracleRequest" in _data. Of course, the function selector here can be customized according to the services provided by the oracle, and it does not have to be "oracleRequest", depending on what kind of interface the oracle exposes to the outside world.
When all these modifiers are checked, the current function caller and the transfer amount are checked to see if they are the same as those in _data. After all these security checks are passed, the current oracle contract is called through a delegatecall. Of course, because the function selector in _data has been checked, it is actually the oracleRequest method that is called.
First, concatenate the oracle requester and the nonce he sent and then hash them as the requestId for this request, and check the commitments mapping to see if it is a unique id. If the check is OK, set an expiration time, add the requestId to the commitments, and concatenate _payment, _callbackAddress, _callbackFunctionId and expiration as the value. Most importantly, issue an OracleRequest event, which contains the request data _data, which is a Concise Binary Object Representation (CBOR) data. The encoding format is lightweight and concise, and can be simply understood as a binary JSON format. This data can be in various forms, depending on how the off-chain nodes are designed.
For example, a Chainlink: ETH/USD Aggregator has a transaction that contains an OracleRequest event:
OracleRequest Event Example
This event shows that the ETH/USD price aggregator 0xF79D6aFBb6dA890132F9D7c355e3015f15F3406F sent a price data request to the oracle: 0x7e94a8a23687d8c7058ba5625db2ce358bcbd244. If the oracle returns the request data, you can know the returned contract address: 0xF79D6aFBb6dA890132F9D7c355e3015f15F3406F, the method ID to be called: 6A9705B4, and the expiration time: 1618185924.
Off-chain node response
3.1 Call fulfillOracleRequest off-chain
First check:
- onlyAuthorizedNode: The function caller (msg.sender) must be the owner of the contract or in the authorized list;
- isValidRequest: still checks whether the requestId exists in the commitments mapping;
- Concatenate payment, callbackAddress, _callbackFunctionId and expiration, and check whether they are the corresponding values of the requestId in the commitments map.
If all these checks pass, the cost of this request is added to withdrawableTokens to record the amount that can be withdrawn. The _requestId is then deleted from the commitments mapping. Finally, the remaining gas amount is calculated to see if it is greater than MINIMUM_CONSUMER_GAS_LIMIT, which is the minimum amount of gas required to execute the callback function of the contract that issued the request.
If all the above checks are passed, the callback function of the requester contract can be formally called in the form of call.
The response to the request should be as fast as possible, so it is recommended to use ZAN's node service ( https://zan.top/home/node-service?chInfo=ch_WZ ) to improve the response speed. You can find the corresponding RPC link in the node service console to increase the speed of sending transactions off-chain.
3.2 Callback Function
We previously learned from oracleRequest that the callback function id is 6A9705B4, and the method is "chainlinkCallback(bytes32,int256"
validateChainlinkCallback is a customizable function, here is a modifier:
In pendingRequests, check whether the oracle corresponding to the request for _requestId matches. And emit the event ChainlinkFulfilled:
If all the checks are passed, then the responses can be further processed, where the answers mapping is updated. If it is a price oracle, the price data of the response is assigned to currentPrice to update the price accordingly:
The above is the complete process of the general oracle service.
Let’s take a “requestEthereumPrice” method in the “TestnetConsumer” contract provided by Chainlink as an example to briefly explain the process of price oracle request response. This function is defined as follows:
The function it implements is to obtain the ETH/USD transaction price from the specified API (cryptocompare). The parameters passed into the function are the specified oracle address and jobId. After a series of request parameters are assembled, the "sendChainlinkRequestTo" method is called to send the request. "sendChainlinkRequestTo" is an interface method defined in the library provided by Chainlink, and is defined as follows:
After receiving the transfer, the Oracle contract will trigger the "onTokenTransfer" method, which will check the validity of the transfer and record more detailed data information by issuing the "OracleRequest" event.
This log will be found in the oracle contract log. The nodes under the chain will subscribe to the log of this topic. After obtaining the recorded log information, the node will parse the specific information of the request and obtain the result of the request through the network API call. Then, by submitting the transaction, the "fulfillOracleRequest" method in the Oracle contract is called to submit the data to the chain.
After performing a series of checks, this method will return the results to the consumer contract through the previously recorded callback address and callback function.
As a developer, I just want to use the existing currency pair prices without specifying these URLs myself. Is that possible?
The answer is yes. The first way to use it is as follows:
First, each trading pair has a separate Price Feed, also called Aggregator, which is actually an AggregatorProxy, as shown below:
The specific implementation of this interface is relatively simple, you can refer to the AAVE/ETH pair: https://etherscan.io/address/0x6Df09E975c830ECae5bd4eD9d90f3A95a4f88012#code
There are 5 query methods in total:
- decimals(): The number of digits of precision of the returned price data, usually 8 or 18
- description(): usually the name of the trading pair, such as ETH / USD
- version(): Mainly used to identify the type of Aggregator that the Proxy points to
- getRoundData(_roundId): Get the current price data based on the round ID
- latestRoundData(): Get the latest price data
In most application scenarios, the contract may only need to read the latest price, that is, call the last method, and the answer in the return parameter is the latest price.
In addition, the price of token reading in most applications is uniformly denominated in USD . If so, you will find that the precision of the pair denominated in USD is uniformly 8 digits, so generally there is no need to deal with different precision issues for different tokens.