FTSO Scaling

May. 25. 2024

BEGINNER
INTERMEDIATE
FTSO

What is FTSOv2 Scaling?

This blogpost will explain how to use the upgraded FTSOv2 data to access more pricing data at a much faster rate. The existing FTSO protocol deployed on both Flare and Songbird allows you to access 18 different price feeds with a refresh rate of 180 seconds. This is a great start but what if you want to access more price feeds? This is where FTSOv2 scaling comes in. FTSOv2 allows you to access more price feeds (currently 51 on Coston) at a much faster rate - the feeds refresh every 90 seconds. This is achieved by using a much more efficient method to reach a consensus on the price feeds that use less gas and decrease the time it takes to reach a consensus. It also allows for more price feeds to be added in the future without storage bloat, enabling the network to scale and meet the needs of dapps that require more price feeds. To learn more about FTSOv2, check out the FTSOv2 documentation.

In this demo, we will see, how you can easily combine FTSOv2 data with the Flare SDK to access price data and sell NFT on Flare with the price that is fixed in external assets like BTC and still be able to pay for the transaction in native FLR tokens.

FTSOv2 scaling

What does the price look like in the scaled version of the system? Previously, we had 18 price feeds that were deposited directly on the chain, but that was expensive and didn't scale well. With the newer version, the FTSO protocol does not deposit full data on the chain, as this produces a lot of unnecessary states and limits the full activity on the network. Instead, we use a much more efficient method, where the FTSO protocol deposits only a Merkle root of all prices gathered in the voting epoch. This allows us to have only a single storage slot on chain, that can efficiently represent the whole pricing data.

Why is this better? This allows us to add new pricing feeds without increasing the storage requirements on the chain and with the relay, this allows Flare to provide the pricing data to other chains and dApps in a much more gas-efficient way, which is also easier on the validators.

Does this make it less secure? Absolutely not. The finalized Merkle root contains all the data and proving a false price would require enormous computational power and is equivalent to breaking the keccak256 hash function - so impossible for now.

Does it make it more difficult to use? Not at all. The Flare's SDKs provide a simple way to work with the new format of the pricing data, verify it against the Relay and check that the provided price is correct and not manipulated. External tooling is also provided to easily access the full pricing information together with proof, which can be used on the chain to verify the validity of the price and then act upon it.

IFTSOFeedVerifier

Let's take a look at the IFTSOFeedVerifier interface that is used to verify the price data.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4interface IFTSOFeedVerifier {
5 struct Feed {
6 uint32 votingRoundId;
7 bytes21 id;
8 int32 value;
9 uint16 turnoutBIPS;
10 int8 decimals;
11 }
12
13 struct FeedWithProof {
14 bytes32[] proof;
15 Feed body;
16 }
17
18 function get_ftso_protocol_feed_id() external view returns (uint256);
19
20 function verifyPrice(
21 FeedWithProof calldata _feed_data
22 ) external view returns (bool);
23}

This interface is used to verify the price data that is provided by the FTSO protocol. Don't worry, the implementation is already available on chain (coston link) and the interface is available in the updated periphery package, from where the code above is copied. FTSO is now part of the FSP protocol and has a specific protocol feed id that distinguishes it from other feeds. In our case, that id is 100, but it's recommended to use the provided getter to get the correct id - so you don't need to remember it.

The most important thing to us is the FeedWithProof struct that contains the price data and the Merkle proof that can be used to verify the price data. The struct contains two fields, where Feed is the price data and proof is the Merkle proof that can be used to verify the price data. The Feed part should be easy to decipher. Compared to the FTSOv1 it contains a lot more information. Now, you can access the price and decimals easily in the same struct, while also getting the voting round id (when the price was selected), the turnout - how many data providers actively provided the price and the id of the feed. If id of the field is a bit confusing, don't worry, we will come back to it later and explain exactly how to use it. Importantly, the price can now be negative, as the protocol can now support assets that have a negative value (EURIBOR looking at you).

So we now have a struct from where we get the price data and the proof to verify it. But how do we do that? Well, we have to make sure, that the Merkle proof together with feed data hashes to exactly the root accepted in the FTSO protocol. One can do this by himself, but you can also used the provided verifyPrice function that does this for you. As the feed already contains the voting round id, the method already knows which root it has to use and we don't need to provide any additional information. For those that are interested in the implementation, it's available in the periphery package and can be found here, but it just calls openzeppelin's MerkleProof library to verify the proof and Relay to get the root.

FeedId

Let's unpack the feed id, this is the unique identifier of the FTSO feed to which the price corresponds to. But how do we get it? Well, you have to hex-encode the feed name as a string to get a representation that is easy to use in solidity. But again, you don't need to do this by yourself. The periphery package provides a library that does it for you.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4// TODO: Check if there are any breaking changes in solidity versions
5library FTSOFeedIdConverter {
6 function FeedCategoryCrypto() internal pure returns (uint8) {
7 return 1;
8 }
9
10 function FeedCategoryForex() internal pure returns (uint8) {
11 return 2;
12 }
13
14 function FeedCategoryCommodity() internal pure returns (uint8) {
15 return 3;
16 }
17
18 function FeedCategoryStock() internal pure returns (uint8) {
19 return 4;
20 }
21
22 /**
23 * Returns the feed id for given category and name.
24 * @param _category Feed category.
25 * @param _name Feed name.
26 * @return Feed id.
27 */
28 function getFeedId(
29 uint8 _category,
30 string memory _name
31 ) internal pure returns (bytes21) {
32 bytes memory nameBytes = bytes(_name);
33 require(nameBytes.length <= 20, "name too long");
34 return bytes21(bytes.concat(bytes1(_category), nameBytes));
35 }
36
37 function getCryptoFeedId(
38 string memory _name
39 ) internal pure returns (bytes21) {
40 return
41 bytes21(bytes.concat(bytes1(FeedCategoryCrypto()), bytes(_name)));
42 }
43
44 function getForexFeedId(
45 string memory _name
46 ) internal pure returns (bytes21) {
47 return bytes21(bytes.concat(bytes1(FeedCategoryForex()), bytes(_name)));
48 }
49
50 function getCommodityFeedId(
51 string memory _name
52 ) internal pure returns (bytes21) {
53 return
54 bytes21(
55 bytes.concat(bytes1(FeedCategoryCommodity()), bytes(_name))
56 );
57 }
58
59 function getStockFeedId(
60 string memory _name
61 ) internal pure returns (bytes21) {
62 return bytes21(bytes.concat(bytes1(FeedCategoryStock()), bytes(_name)));
63 }
64
65 /**
66 * Returns the feed category and name for given feed id.
67 * @param _feedId Feed id.
68 * @return _category Feed category.
69 * @return _name Feed name.
70 */
71 function getFeedCategoryAndName(
72 bytes21 _feedId
73 ) internal pure returns (uint8 _category, string memory _name) {
74 _category = uint8(_feedId[0]);
75 uint256 length = 20;
76 while (length > 0) {
77 if (_feedId[length] != 0x00) {
78 break;
79 }
80 length--;
81 }
82 bytes memory nameBytes = new bytes(length);
83 for (uint256 i = 0; i < length; i++) {
84 nameBytes[i] = _feedId[i + 1];
85 }
86 _name = string(nameBytes);
87 }
88}

It might look intimidating, but it's actually quite simple. The first byte is the category of the feed and the rest is the name of the feed. In our case, we have 4 categories - Crypto, Forex, Commodity and Stock and we will use the Crypto category (01) for our example. To get the price of feed that provides FLR price in USD, one should just call FTSOFeedIdConverter.getCryptoFeedId("FLR/USD") and the result would be 01464c522f55534400000000000000000000000000 (zero padded to 21 bytes). Similarly, the BTC/USD price feed would be 014254432f55534400000000000000000000000000. If you want to easily check what feeds are available, you can check the contracts yourself, or just hop on the system explorer where you can also see the feed ids and the prices.

Show me solidity code

Ok, let's put it all together. The main goal is simple - the user should be able to buy NFT on Flare, which is priced at 1 ETH and pay for it in FLR. To do this, our contract will need to convert from ETH price, to $ price and then express it in FLR (in wei). We will use the FTSOv2 data to get the price of ETH/USD and FLR/USD and then calculate the price of ETH in FLR and then check if the user has paid enough to buy the NFT.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
5import {FTSOFeedIdConverter} from "@flarenetwork/flare-periphery-contracts/coston/util-contracts/FTSOFeedIdConverter.sol";
6import {FlareContractsRegistryLibrary} from "@flarenetwork/flare-periphery-contracts/coston/util-contracts/ContractRegistryLibrary.sol";
7import {IRelay} from "@flarenetwork/flare-periphery-contracts/coston/util-contracts/userInterfaces/IRelay.sol";
8import {IFTSOFeedVerifier} from "@flarenetwork/flare-periphery-contracts/coston/util-contracts/userInterfaces/IFTSOFeedVerifier.sol";
9
10struct NFTInfo {
11 string name;
12 string FTSOsymbol;
13 int256 basePrice;
14 string uri;
15}
16
17contract FTSOV2NFT is ERC721URIStorage {
18 uint256 public currentIndex = 0;
19 address public owner;
20 uint256 public constant PRICE_BASE = 10;
21 uint256 public MAX_HISTORICAL_VOTING_EPOCHS = 4;
22
23 NFTInfo[] public availableOptions;
24 mapping(string => uint256) public optionIndex;
25
26 constructor(string memory name, string memory symbol) ERC721(name, symbol) {
27 owner = msg.sender;
28
29 availableOptions.push(
30 NFTInfo({
31 name: "BTC",
32 FTSOsymbol: "BTC/USD",
33 basePrice: -6,
34 uri: "https://cloudflare-ipfs.com/ipfs/QmVFyMRLgFoCTxFZLZb793Ji79qzrPhnFEBro5o7VYmxmm"
35 })
36 );
37 availableOptions.push(
38 NFTInfo({
39 name: "DOGE",
40 FTSOsymbol: "DOGE/USD",
41 basePrice: 1,
42 uri: "https://cloudflare-ipfs.com/ipfs/QmV6mL9F6KFJ8rYX3ARmUm8xEo9GQUhM5Udd6fkKeAWrLu"
43 })
44 );
45 availableOptions.push(
46 NFTInfo({
47 name: "XRP",
48 FTSOsymbol: "XRP/USD",
49 basePrice: 2,
50 uri: "https://cloudflare-ipfs.com/ipfs/QmVFyMRLgFoCTxFZLZb793Ji79qzrPhnFEBro5o7VYmxmm"
51 })
52 );
53 availableOptions.push(
54 NFTInfo({
55 name: "BNB",
56 FTSOsymbol: "BNB/USD",
57 basePrice: -3,
58 uri: "https://cloudflare-ipfs.com/ipfs/QmV6mL9F6KFJ8rYX3ARmUm8xEo9GQUhM5Udd6fkKeAWrLu"
59 })
60 );
61 availableOptions.push(
62 NFTInfo({
63 name: "ARB",
64 FTSOsymbol: "ARB/USD",
65 basePrice: 0,
66 uri: "https://cloudflare-ipfs.com/ipfs/QmVFyMRLgFoCTxFZLZb793Ji79qzrPhnFEBro5o7VYmxmm"
67 })
68 );
69 availableOptions.push(
70 NFTInfo({
71 name: "ETH",
72 FTSOsymbol: "ETH/USD",
73 basePrice: 0,
74 uri: "https://cloudflare-ipfs.com/ipfs/QmV6mL9F6KFJ8rYX3ARmUm8xEo9GQUhM5Udd6fkKeAWrLu"
75 })
76 );
77
78 for (uint256 i = 0; i < availableOptions.length; i++) {
79 optionIndex[availableOptions[i].FTSOsymbol] = i + 1;
80 }
81 }
82
83 function getRelay() public view returns (IRelay) {
84 return FlareContractsRegistryLibrary.getRelay();
85 }
86
87 function getAvailableOptions() public view returns (NFTInfo[] memory) {
88 return availableOptions;
89 }
90
91 function getPriceInFlare(
92 string memory quoteCurrency,
93 IFTSOFeedVerifier.FeedWithProof calldata quoteCurrencyPrice,
94 IFTSOFeedVerifier.FeedWithProof calldata baseCurrencyPrice
95 ) public view returns (uint256) {
96 if (quoteCurrencyPrice.proof.length >= 1) {
97 require(
98 checkCorrectness(quoteCurrencyPrice),
99 "Invalid quote currency price"
100 );
101 }
102 if (baseCurrencyPrice.proof.length >= 1) {
103 require(
104 checkCorrectness(baseCurrencyPrice),
105 "Invalid base currency price"
106 );
107 }
108 // Check that the base currency is FLR
109 require(
110 baseCurrencyPrice.body.id ==
111 FTSOFeedIdConverter.getCryptoFeedId("FLR/USD"),
112 "Invalid base currency"
113 );
114 // Check that the quote currency is the same as provided proof
115 require(
116 FTSOFeedIdConverter.getCryptoFeedId(quoteCurrency) ==
117 quoteCurrencyPrice.body.id,
118 "Invalid price feed"
119 );
120 require(
121 quoteCurrencyPrice.body.votingRoundId ==
122 baseCurrencyPrice.body.votingRoundId,
123 "Voting round mismatch"
124 );
125
126 uint256 currentVotingRoundId = getCurrentVotingRoundId();
127
128 require(
129 quoteCurrencyPrice.body.votingRoundId +
130 MAX_HISTORICAL_VOTING_EPOCHS >=
131 currentVotingRoundId,
132 "Voting round too old"
133 );
134
135 int8 decimalDifference = baseCurrencyPrice.body.decimals -
136 quoteCurrencyPrice.body.decimals;
137
138 uint256 index = optionIndex[quoteCurrency];
139 require(index != 0, "Invalid quote currency");
140
141 NFTInfo memory nftInfo = availableOptions[index - 1];
142
143 int256 nftPrice = nftInfo.basePrice;
144 uint256 priceInFlare = 1 ether;
145 if (nftPrice > 0) {
146 priceInFlare = priceInFlare * (PRICE_BASE ** uint256(nftPrice));
147 }
148 priceInFlare =
149 (priceInFlare * uint32(quoteCurrencyPrice.body.value)) /
150 uint32(baseCurrencyPrice.body.value);
151
152 if (decimalDifference > 0) {
153 priceInFlare = priceInFlare * (10 ** uint8(decimalDifference));
154 } else if (decimalDifference < 0) {
155 priceInFlare = priceInFlare / (10 ** uint8(-decimalDifference));
156 }
157 if (nftPrice < 0) {
158 priceInFlare = priceInFlare / (PRICE_BASE ** uint256(-nftPrice));
159 }
160 return priceInFlare;
161 }
162
163 function buyNFT(
164 string memory quoteCurrency,
165 IFTSOFeedVerifier.FeedWithProof calldata quoteCurrencyPrice,
166 IFTSOFeedVerifier.FeedWithProof calldata baseCurrencyPrice
167 ) public payable {
168 uint256 price = getPriceInFlare(
169 quoteCurrency,
170 quoteCurrencyPrice,
171 baseCurrencyPrice
172 );
173 require(checkCorrectness(quoteCurrencyPrice), "Invalid quote proof");
174 require(checkCorrectness(baseCurrencyPrice), "Invalid base proof");
175 require(msg.value >= price, "Insufficient funds");
176 _safeMint(msg.sender, currentIndex);
177 uint256 index = optionIndex[quoteCurrency];
178 require(index != 0, "Invalid quote currency");
179 NFTInfo memory nftInfo = availableOptions[index - 1];
180 _setTokenURI(currentIndex, nftInfo.uri);
181 ++currentIndex;
182 }
183
184 function getCurrentVotingRoundId() public view returns (uint256) {
185 return getRelay().getVotingRoundId(block.timestamp);
186 }
187
188 function getSafeVotingRoundId() public view returns (uint256) {
189 uint256 currentVotingEpoch = getCurrentVotingRoundId();
190 IRelay relay = getRelay();
191 for (uint256 i = 0; i < MAX_HISTORICAL_VOTING_EPOCHS; i++) {
192 bytes32 root = relay.getConfirmedMerkleRoot(
193 FlareContractsRegistryLibrary
194 .auxiliaryGetIFTSOFeedVerifier()
195 .get_ftso_protocol_feed_id(),
196 currentVotingEpoch - i
197 );
198 if (root != 0) {
199 return currentVotingEpoch - i;
200 }
201 }
202 revert("No safe voting epoch found");
203 }
204
205 function checkCorrectness(
206 IFTSOFeedVerifier.FeedWithProof calldata _feed_data
207 ) public view returns (bool) {
208 return
209 FlareContractsRegistryLibrary
210 .auxiliaryGetIFTSOFeedVerifier()
211 .verifyPrice(_feed_data);
212 }
213
214 function pullFunds() public {
215 require(msg.sender == owner, "Only owner can pull funds");
216 payable(msg.sender).transfer(address(this).balance);
217 }
218}

The code seems quite long, but most of it is boilerplate and the autoformatter makes a bit too many lines. Still, we create a mapping and list that contains information about available NFTs, their base price and the price feed that we will use to price them (together with metadata URI - the most important part).

The most important part is the getPriceInFlare, which receives two prices - the quote currency price and the base currency price and calculates the price of the NFT in FLR. Pay attention to the first two checks - the user is supplying us with the price data and we need to make sure that the data is correct. We do not check the price if there is no proof - just to make it simpler for users to get the price in FLR without the need to provide the proof, but we most definitely check the correctness of the proof if it is provided and require the proof when buying. Checking is simple - we just delegate it to the library that we have mentioned before. But checking is also mandatory - this is the security we are after. Anyone can provide (possibly incorrect) pricing data, but we require that the proof matches the Merkle root found on the chain and that one cannot be tampered with. Why is this important? Even if we get a malicious user or get data from a malicious source, the on-chain checking will always revert it and there is no incentive to provide false data.

The rest of the function contains some simple checks - that the provided price is for FLR, that the quote currency is the same as the one provided in the function and that the voting round is not too old. Then we calculate the price of the NFT in FLR with some gimmicks to handle a larger range of prices and decimals.

Buying NFT

Buying NFT is simple as seen in the following code:

1 function buyNFT(
2 string memory quoteCurrency,
3 IFTSOFeedVerifier.FeedWithProof calldata quoteCurrencyPrice,
4 IFTSOFeedVerifier.FeedWithProof calldata baseCurrencyPrice
5 ) public payable {
6 uint256 price = getPriceInFlare(
7 quoteCurrency,
8 quoteCurrencyPrice,
9 baseCurrencyPrice
10 );
11 require(checkCorrectness(quoteCurrencyPrice), "Invalid quote proof");
12 require(checkCorrectness(baseCurrencyPrice), "Invalid base proof");
13 require(msg.value >= price, "Insufficient funds");
14 _safeMint(msg.sender, currentIndex);
15 uint256 index = optionIndex[quoteCurrency];
16 require(index != 0, "Invalid quote currency");
17 NFTInfo memory nftInfo = availableOptions[index - 1];
18 _setTokenURI(currentIndex, nftInfo.uri);
19 ++currentIndex;
20 }

The user has to provide us with two prices - from which we calculate the final price of the NFT in FLR. We then just check that the prices indeed come with proofs and that the user has paid enough. Then we just mint the NFT and set the metadata uri.

Relay

The Relay is a contract that is used to store the Merkle roots of all the FSP protocols - in our example, we use it to query FTSO protocol root.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4/**
5 * Relay interface.
6 */
7interface IRelay {
8
9 // Event is emitted when a new signing policy is initialized by the signing policy setter.
10 event SigningPolicyInitialized(
11 uint24 indexed rewardEpochId, // Reward epoch id
12 uint32 startVotingRoundId, // First voting round id of validity.
13 // Usually it is the first voting round of reward epoch rewardEpochId.
14 // It can be later,
15 // if the confirmation of the signing policy on Flare blockchain gets delayed.
16 uint16 threshold, // Confirmation threshold (absolute value of noramalised weights).
17 uint256 seed, // Random seed.
18 address[] voters, // The list of eligible voters in the canonical order.
19 uint16[] weights, // The corresponding list of normalised signing weights of eligible voters.
20 // Normalisation is done by compressing the weights from 32-byte values to
21 // 2 bytes, while approximately keeping the weight relations.
22 bytes signingPolicyBytes, // The full signing policy byte encoded.
23 uint64 timestamp // Timestamp when this happened
24 );
25
26 // Event is emitted when a signing policy is relayed.
27 // It contains minimalistic data in order to save gas. Data about the signing policy are
28 // extractable from the calldata, assuming prefered usage of direct top-level call to relay().
29 event SigningPolicyRelayed(
30 uint256 indexed rewardEpochId // Reward epoch id
31 );
32
33 // Event is emitted when a protocol message is relayed.
34 event ProtocolMessageRelayed(
35 uint8 indexed protocolId, // Protocol id
36 uint32 indexed votingRoundId, // Voting round id
37 bool isSecureRandom, // Secure random flag
38 bytes32 merkleRoot // Merkle root of the protocol message
39 );
40
41 /**
42 * Finalization function for new signing policies and protocol messages.
43 * It can be used as finalization contract on Flare chain or as relay contract on other EVM chain.
44 * Can be called in two modes. It expects calldata that is parsed in a custom manner.
45 * Hence the transaction calls should assemble relevant calldata in the 'data' field.
46 * Depending on the data provided, the contract operations in essentially two modes:
47 * (1) Relaying signing policy. The structure of the calldata is:
48 * function signature (4 bytes) + active signing policy
49 * + 0 (1 byte) + new signing policy,
50 * total of exactly 4423 bytes.
51 * (2) Relaying signed message. The structure of the calldata is:
52 * function signature (4 bytes) + signing policy
53 * + signed message (38 bytes) + ECDSA signatures with indices (67 bytes each)
54 * Reverts if relaying is not successful.
55 */
56 function relay() external;
57
58 /**
59 * Returns the signing policy hash for given reward epoch id.
60 * @param _rewardEpochId The reward epoch id.
61 * @return _signingPolicyHash The signing policy hash.
62 */
63 function toSigningPolicyHash(uint256 _rewardEpochId) external view returns (bytes32 _signingPolicyHash);
64
65 /**
66 * Returns the Merkle root for given protocol id and voting round id.
67 * @param _protocolId The protocol id.
68 * @param _votingRoundId The voting round id.
69 * @return _merkleRoot The Merkle root.
70 */
71 function merkleRoots(uint256 _protocolId, uint256 _votingRoundId) external view returns (bytes32 _merkleRoot);
72
73 /**
74 * Returns the start voting round id for given reward epoch id.
75 * @param _rewardEpochId The reward epoch id.
76 * @return _startingVotingRoundId The start voting round id.
77 */
78 function startingVotingRoundIds(uint256 _rewardEpochId) external view returns (uint256 _startingVotingRoundId);
79
80 /**
81 * Returns the current random number, its timestamp and the flag indicating if it is secure.
82 * @return _randomNumber The current random number.
83 * @return _isSecureRandom The flag indicating if the random number is secure.
84 * @return _randomTimestamp The timestamp of the random number.
85 */
86 function getRandomNumber()
87 external view
88 returns (
89 uint256 _randomNumber,
90 bool _isSecureRandom,
91 uint256 _randomTimestamp
92 );
93
94 /**
95 * Returns the voting round id for given timestamp.
96 * @param _timestamp The timestamp.
97 * @return _votingRoundId The voting round id.
98 */
99 function getVotingRoundId(uint256 _timestamp) external view returns (uint256 _votingRoundId);
100
101 /**
102 * Returns the confirmed merkle root for given protocol id and voting round id.
103 * @param _protocolId The protocol id.
104 * @param _votingRoundId The voting round id.
105 * @return _merkleRoot The confirmed merkle root.
106 */
107 function getConfirmedMerkleRoot(uint256 _protocolId, uint256 _votingRoundId)
108 external view
109 returns (bytes32 _merkleRoot);
110
111 /**
112 * Returns last initialized reward epoch data.
113 * @return _lastInitializedRewardEpoch Last initialized reward epoch.
114 * @return _startingVotingRoundIdForLastInitializedRewardEpoch Starting voting round id for it.
115 */
116 function lastInitializedRewardEpochData()
117 external view
118 returns (
119 uint32 _lastInitializedRewardEpoch,
120 uint32 _startingVotingRoundIdForLastInitializedRewardEpoch
121 );
122}

The most important method for us is function getConfirmedMerkleRoot(uint256 _protocolId, uint256 _votingRoundId), which returns the Merkle root for the given protocol id and voting round id - exactly the root we need to verify the price data. It is exactly the one that is internally used by the IFTSOFeedVerifier to verify the price data. Another one that we use is function getVotingRoundId(uint256 _timestamp), which gives us the voting round id for the given timestamp. Why do we use it? To get the most fresh price we can get.

Remember, in the old FTSo system, we had to wait for 90s after each price epoch for the price to get finalized, but now, the finalization can be much faster, as the data providers will finalize the price as soon as all the data is available, which can be even faster than in 30s. To do this (and to allow fronted to interact with the contract easier), we have implemented function getSafeVotingRoundId() public view returns (uint256), which gets current voting round id and then goes back in time to get the most recent finalized price. This is important, as we want to make sure that the price we are using is the most recent one - and get is as soon as it is finalized. To be a bit more safe, we have MAX_HISTORICAL_VOTING_EPOCHS that limits how far back we can go to get the finalized price - if there is no price in the last few rounds, we will rather wait to get a bit more fresh price.

How to get the price and proof?

To get the price and proof struct off-chain, you need to do some calculations or get to the API that does it for you. Fortunately, every data provider will have this information already available, so if they run an open API, you can try and get it there. Another possibility is to use Flare's provided API that is available on TODO.

Once you have the API, it is simple - the endpoint /specific-feed/SYMBOL/EPOCH returns exactly the needed proof, that can be directly fed into the contract. A simple CURL call then looks like this:

1curl -X 'GET' \
2 'https://API_URL/specific-feed/0x014254432f55534400000000000000000000000000/648538' \
3 -H 'accept: application/json' \
4 -H 'X-API-KEY: XXX-XXX-XXX-X'

and returns

1{
2 "status": "OK",
3 "feedWithProof": {
4 "body": {
5 "votingRoundId": 648538,
6 "id": "0x014254432f55534400000000000000000000000000",
7 "value": 6885054,
8 "turnoutBIPS": 9999,
9 "decimals": 2
10 },
11 "proof": [
12 "0xa2da758620f31407933462306a2d45120e70a3db971d95486a3d69cbc497f6bc",
13 "0xa453ae63c3b84dc8239f1bff59a41c2a9833add385266808423463b9edaa7cbd",
14 "0xbdd2d87b8335993031a643c22e1a45d7745902e929e655f04a044b248b9302c7",
15 "0xcf94c0655b58f0a74bcc97cea9a6e8556cf0849871b9b868268e0e80a223ef1f",
16 "0x92d7c8921dadfbf8c4521bdcc38ba240e51830972929185b2b1e8aaba707fa4c",
17 "0xc2d4e23727d8d90b95b85e8c1c437f69ece3f378a72773260457ba2d4040e41a"
18 ]
19 }
20}