Để lấy dữ liệu từ các data feed, chúng ta chỉ cần đọc dữ liệu từ oracle contract được cung cấp bởi Chainlink.
Importing
Chúng ta giao tiếp với oracle contract bằng cách sử dụng interface AggregatorV3Interface. Interface này có thể được import trực tiếp từ GitHub (vào Remix) hoặc thông qua NPM (nếu dùng các IDE khác).
Đối với Foundry, ta có thể install như sau:
forge install smartcontractkit/chainlink-brownie-contractsVà thêm mapping như sau:
remappings = [
"@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts",
]Cú pháp import:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";AggregatorV3Interface
Sau khi import interface thì ta cần biết địa chỉ của oracle contract cần sử dụng1. Chẳng hạn nếu ta muốn biết tỷ giá giữa ETH và USD của mạng Sepolia thì địa chỉ của contract sẽ là: 0x694AA1769357215DE4FAC081bf1f309aDC325306.
Seealso
Danh sách các địa chỉ của các contract giữ các loại tỷ giá khác: Price Feed Contract Addresses | Chainlink Documentation
Ta tạo ra một consumer contract như sau:
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumerV3 {
AggregatorV3Interface internal priceFeed;
constructor() {
priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
}
}Price Feed
Để lấy tỷ giá từ price feed, ta cần gọi sử dụng hàm latestRoundData. Hàm này sẽ trả về một tuple gồm những thông tin sau đây:
function latestRoundData() external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);Trong đó, answer chính là tỷ giá.
Ta viết một hàm để lấy tỷ giá mới nhất và lưu vào biến price như sau:
function getLatestPrice() public view returns (int) {
(
/* uint80 roundId */,
int price,
/*uint startedAt*/,
/*uint updatedAt*/,
/*uint80 answeredInRound*/
) = dataFeed.latestRoundData();
return price;
}Giá trị nhận được khi gọi hàm có thể là:
186164000000Có thể thấy, giá trị này không chứa dấu phẩy hay dấu chấm nên ta không biết được phần nào là phần nguyên và phần nào là phần thập phân.
Ta có thể gọi sử dụng hàm decimals của price feed để biết được có bao nhiêu chữ số sau ở phần thập phân như sau:
function getDecimals() public view returns (uint8) {
uint8 decimals = priceFeed.decimals();
return decimals;
}Nếu kết quả là 8 thì ta biết được 1 ETH tương đương với 1861 USD.
Working with the msg.value
Giá trị price trả về từ latestRoundData chỉ có 8 chữ số sau phần thập phân (ta gọi là 1e8 precision). Để giá trị này tương thích với msg.value (1e18 precision), ta cần phải nhân giá trị này với 1e10 (tức là 10000000000)
return price * 1e10;Ngoài ra, msg.value có kiểu là uint256 nên nếu muốn tương thích với msg.value thì cần ép kiểu giá trị trả về từ getLatestPrice sang uint256:
return uint256(price * 1e10);Preserving Precision
Khi thực hiện phép chia mà muốn đảm bảo độ chính xác cao, ta nên thực hiện phép nhân trước phép chia:
Tip
Always multiply before dividing to maintain precision and avoid truncation errors. For instance, in floating-point arithmetic,
(5/3) * 2equals approximately 3.33. In Solidity,(5/3)equals 1, which when multiplied by 2 yields 2. If you multiply first(5*2)and then divide by 3, you achieve better precision.
Stale Check
Để đảm bảo price feed luôn được cập nhật mới nhất, ta có thể tính thời gian trôi qua kể từ lúc answer của price feed được cập nhật và so sánh nó với heartbeat - là một đại lượng cho biết thời gian tối đa có thể trôi qua trước khi price feed cần cập nhật (ví dụ đối với ETH/USD, heartbeat là 3600 giây, xem thêm ở Price Feed Contract Addresses | Chainlink Documentation). Nếu thời gian trôi qua lớn hơn heartbeat thì ta coi answer là stale và không sử dụng giá trị này.
Để hiện thực, ta sẽ xây dựng thư viện OracleLib trong file src/libraries/OracleLib.sol như sau:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
/**
* @title OracleLib
* @author Patrick Collins
* @notice This library is used to check the Chainlink Oracle for stale data.
* If a price is stale, functions will revert, and render the DSCEngine unusable - this is by design.
* We want the DSCEngine to freeze if prices become stale.
*
* So if the Chainlink network explodes and you have a lot of money locked in the protocol... too bad.
*/
library OracleLib {
error OracleLib__StalePrice();
uint256 private constant TIMEOUT = 3 hours;
function staleCheckLatestRoundData(
AggregatorV3Interface pricefeed
) public view returns (uint80, int256, uint256, uint256, uint80) {
(
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = pricefeed.latestRoundData();
uint256 secondsSince = block.timestamp - updatedAt;
if (secondsSince > TIMEOUT) revert OracleLib__StalePrice();
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}
}Ở đây, hàm staleCheckLatestRoundData sẽ là một wrapper function của latestRoundData được dùng để đảm bảo answer từ price feed không bị cũ. Ta thực hiện điều này bằng cách tính secondsSince và so sánh với TIMEOUT có giá trị là 3 giờ (ta rộng lượng sử dụng giá trị này thay vì dùng 3600 giây). Nếu giá trị secondsSince lớn hơn TIMEOUT thì ta sẽ revert và không sử dụng giá trị này. Ngược lại, trả về các giá trị của latestRoundData.
Smart contract sử dụng thư viện có thể declare như sau:
import {OracleLib} from "./libraries/OracleLib.sol";
// ...
contract DSCEngine is ReentrancyGuard {
// ...
using OracleLib for AggregatorV3Interface;
// ...
}
// ...Sau đó, thay thế tất cả các lời gọi hàm đến latestRoundData thành staleCheckLatestRoundData.
Resources
Footnotes
-
Tham khảo thêm Chainlink Data Feeds | Chainlink Documentation. ↩