Chainlink provides three distinct services:
- Randomness VRF (Verifiable Random Function
- Is a provably-fair and verifiable source of randomness designed for smart contracts
- API Jobs
- Allows smart contract to make a request to an external API
- Price Feeds
- Smart contracts with a periodically updated value representing a price
We will only focus on the price feed service. The price feed service relies on off chain services, and a set of smart contracts. The way the smart contracts function is what will gives us most clues to understand who gets paid, and who pays.
There are multiple price feeds which are listed in the Chainlink Market. An example of a specific price feed would be the ETH/USD. In the price feed page we are even able to get details about the nodes that submit values to the price feed.
For the ETH/USD price feed there are two relevant smart contracts (listed in the page), and a token:
- Flux Aggregator
- Where the functionality lives
- Flux Aggregator Proxy
- More a router than a proxy. A router is a contract that has no functionality, but only does safety checks for protection of the client.
- Link Token
- The token used to pay the data providers
In the example of ETH/USD.
- Multiple nodes send a value to the Flux Aggregator (or Flux Aggregator Proxy). This is what is called a round.
- Once enough values have been received by the Flux Aggregator. The average is calculated, and the round is closed.
- After the round is closed the withdrawal amount available is updated. After this the nodes can withdraw the corresponding amount of Link Token.
The funds paid to the nodes are from the Link Token, and are escrowed in the Flux Aggregator address.
The assumption is that there are sponsors that buy funds into the Flux Aggregator address, in order to keep nodes motivated.
It is free for a smart contract to access the data in the contract.
This can be checked by looking into the function getAnswer()
of the Flux Aggregator Proxy and Flux Aggregator.
FluxAggregator.sol
is the implementation of the Flux Aggregator.
function getAnswer(uint256 _roundId)
public
view
virtual
override
returns (int256)
{
if (validRoundId(_roundId)) {
return rounds[uint32(_roundId)].answer;
}
return 0;
}
AccessControledAggregator.sol
is the implementation of the Flux Aggregator Proxy.
function getAnswer(uint256 _roundId)
public
view
override
checkAccess()
returns (int256)
{
return super.getAnswer(_roundId);
}
(We will ignore the Flux Aggregator Proxy contract, since it only does checks and does not alter the state)
The nodes (referred by oracles sometimes in the code) submit one value per round. To submit this value they send a transaction to call the submit()
function in the Flux Aggregator.
We can find the submit implementation in the FluxAggregator.sol
.
function submit(uint256 _roundId, int256 _submission)
external
{
bytes memory error = validateOracleRound(msg.sender, uint32(_roundId));
require(_submission >= minSubmissionValue, "value below minSubmissionValue");
require(_submission <= maxSubmissionValue, "value above maxSubmissionValue");
require(error.length == 0, string(error));
oracleInitializeNewRound(uint32(_roundId));
recordSubmission(_submission, uint32(_roundId));
(bool updated, int256 newAnswer) = updateRoundAnswer(uint32(_roundId));
payOracle(uint32(_roundId));
deleteRoundDetails(uint32(_roundId));
if (updated) {
validateAnswer(uint32(_roundId), newAnswer);
}
}
The function recordSubmission()
and payOracle()
can be found in the FluxAggregator.sol
, and are called by the submit()
function in order to update the current answer (price value) and mark the withdrawable amount by the nodes (oracles).
function payOracle(uint32 _roundId)
private
{
uint128 payment = details[_roundId].paymentAmount;
Funds memory funds = recordedFunds;
funds.available = funds.available.sub(payment);
funds.allocated = funds.allocated.add(payment);
recordedFunds = funds;
oracles[msg.sender].withdrawable = oracles[msg.sender].withdrawable.add(payment);
emit AvailableFundsUpdated(funds.available);
}
function recordSubmission(int256 _submission, uint32 _roundId)
private
{
require(acceptingSubmissions(_roundId), "round not accepting submissions");
details[_roundId].submissions.push(_submission);
oracles[msg.sender].lastReportedRound = _roundId;
oracles[msg.sender].latestSubmission = _submission;
emit SubmissionReceived(_submission, _roundId, msg.sender);
}
To make clear. The answer is only updated once a certain number of submissions have been made on the round as can be seen on FluxAggregator.sol
.
function updateRoundAnswer(uint32 _roundId)
internal
returns (bool, int256)
{
if (details[_roundId].submissions.length < details[_roundId].minSubmissions) {
return (false, 0);
}
int256 newAnswer = Median.calculateInplace(details[_roundId].submissions);
rounds[_roundId].answer = newAnswer;
rounds[_roundId].updatedAt = uint64(block.timestamp);
rounds[_roundId].answeredInRound = _roundId;
latestRoundId = _roundId;
emit AnswerUpdated(newAnswer, _roundId, now);
return (true, newAnswer);
}
After making a submission and having funds allocated to the nodes, the nodes can withdraw the funds.
In the FluxAggregator.sol
there are two functions that allow us to make transfers of funds escrowed in the aggregator to other accounts. The withdrawPayment()
function allows nodes to transfer funds to any account. The withdrawFunds()
function allows the Flux Aggregator owner to transfer funds to any account.
function withdrawPayment(address _oracle, address _recipient, uint256 _amount)
external
{
require(oracles[_oracle].admin == msg.sender, "only callable by admin");
// Safe to downcast _amount because the total amount of LINK is less than 2^128.
uint128 amount = uint128(_amount);
uint128 available = oracles[_oracle].withdrawable;
require(available >= amount, "insufficient withdrawable funds");
oracles[_oracle].withdrawable = available.sub(amount);
recordedFunds.allocated = recordedFunds.allocated.sub(amount);
assert(linkToken.transfer(_recipient, uint256(amount)));
}
function withdrawFunds(address _recipient, uint256 _amount)
external
onlyOwner()
{
uint256 available = uint256(recordedFunds.available);
require(available.sub(requiredReserve(paymentAmount)) >= _amount, "insufficient reserve funds");
require(linkToken.transfer(_recipient, _amount), "token transfer failed");
updateAvailableFunds();
}
To actually understand how all is working, a Jupyter Notebook was made to check the flow of LINK token and the amount of submissions made (this notebook can be extended and improved, but for now it is enough to prove our assumptions of the flow).
The notebook can be found available on a public gist and it relies on the ETH/USD price feed (all the other price feeds use the same contract but deployed to other addresses).
On 07/03/2021 the Flux Aggregator contract had 66,963.67678707543
LINK tokens escrowed. Out of that 47289.89
LINK tokens are available to be used for future payments, and 19673.78678707542
LINK tokens are already allocated to nodes to be withdrawn.
Most rounds have 20 submissions, although there have been rounds with less submissions. Although rounds with less submissions I don't think are closed (I would need to check that for sure in the code).
The maximum amount of submissions a single node has made is 23187 the minimum has been 1.
Oracle Address | Total Submission Round | First Block Number | First Timestamp | Last Block Number | Last Timestamp |
---|---|---|---|---|---|
0xaaeed1e72a10214c705467316779272e4ba78433 | 1 | 10677688 | 2020-08-17 12:55:25 | 10677688 | 2020-08-17 12:55:25 |
0xff2b320cf904d78eae7298ed10ec143c5e7e2314 | 21508 | 10702986 | 2020-08-21 10:18:09 | 11987359 | 2021-03-06 21:39:05 |
0x87bf62ec62f872a4c74b1a39bb143702c5364785 | 21622 | 10703605 | 2020-08-21 12:37:06 | 11987357 | 2021-03-06 21:38:10 |
0xabbabb17965310949842323f06eede92fd787d0d | 22094 | 10687730 | 2020-08-19 02:04:36 | 11987358 | 2021-03-06 21:38:50 |
0x1edb9539d67b0ceb929ae1f334a6c24499ae9cb9 | 22437 | 10689906 | 2020-08-19 10:02:39 | 11987358 | 2021-03-06 21:38:50 |
0xcf2d187d3833dd9063b019d0c39e4566576c3c56 | 22580 | 10683198 | 2020-08-18 09:37:28 | 11987358 | 2021-03-06 21:38:50 |
0xa14ebb5909fba95a9305cbcaa6ea069733fcd443 | 22695 | 10685189 | 2020-08-18 16:52:03 | 11987358 | 2021-03-06 21:38:50 |
0x2183464128d6f4020cb4c1c908bae2e87f83214a | 22743 | 10682213 | 2020-08-18 05:57:44 | 11987358 | 2021-03-06 21:38:50 |
0x6a9dec02a37f08a7420186c8d273221fd462f7e8 | 22759 | 10679200 | 2020-08-17 18:39:13 | 11987358 | 2021-03-06 21:38:50 |
0x1d0f12e3546e4734d7521a46ed02ef2c2df51e59 | 22774 | 10677391 | 2020-08-17 11:47:01 | 11987358 | 2021-03-06 21:38:50 |
0xac94a69b0996c8edcf19cb7548a644836cb77509 | 22776 | 10996841 | 2020-10-05 16:36:40 | 11987358 | 2021-03-06 21:38:50 |
0x6d1b8ee3d1b5834dde3ae8ba6d7a587ae6d14dc3 | 22808 | 10679636 | 2020-08-17 20:13:48 | 11987358 | 2021-03-06 21:38:50 |
0x98db8c499016aa9d860fcd1a9b75a1d7ef3fc3b0 | 22821 | 10677318 | 2020-08-17 11:32:10 | 11987358 | 2021-03-06 21:38:50 |
0xc8f53fae35468100df31901e939d8ff80b667166 | 22851 | 10996841 | 2020-10-05 16:36:40 | 11987358 | 2021-03-06 21:38:50 |
0xd45727e3d7405c6ab3b2b3a57474012e1f998483 | 22883 | 10609170 | 2020-08-06 23:16:56 | 11987358 | 2021-03-06 21:38:50 |
0x9ab2a7af33d50738caf23e9283e584d954dfb287 | 22886 | 10677281 | 2020-08-17 11:24:58 | 11987358 | 2021-03-06 21:38:50 |
0xc7310123914f624da9c376f8ec590055e62733c1 | 22986 | 10610214 | 2020-08-07 03:14:06 | 11987358 | 2021-03-06 21:38:50 |
0x2ad9b7b9386c2f45223ddfc4a4d81c2957bae19a | 23037 | 10639355 | 2020-08-11 15:17:08 | 11987358 | 2021-03-06 21:38:50 |
0xc4a92358757ef8d22580c5efed30d5241ac725ae | 23108 | 10612042 | 2020-08-07 10:05:03 | 11987358 | 2021-03-06 21:38:50 |
0xf687bfa503376e088c6032cd277eebdf19af4c7d | 23132 | 10613041 | 2020-08-07 13:49:03 | 11987358 | 2021-03-06 21:38:50 |
0x501698a6f6f762c79e4d28e3815c135e3f9af996 | 23157 | 10609429 | 2020-08-07 00:18:37 | 11987358 | 2021-03-06 21:38:50 |
0x0699a397c3cf614c9a7db23a4be28fc4c8f3a755 | 23187 | 10609170 | 2020-08-06 23:16:56 | 11987358 | 2021-03-06 21:38:50 |
Initially the ETH/USD Flux Aggregator contract was funded by Chainlink. Later it became a sponsored price feed.
In the Chainlink feeds page a list of sponsors can be seen.
Sponsor |
---|
Synthetix |
Loopring |
OpenLaw |
1inch |
ParaSwap |
MCDEX |
Futureswap |
DMM |
Aave |
The Force Protocol |
DigiTix |
Consensus Cell |
ENS |
Sandbank |
Plasm Network |
Swipe |
HXRO |
DODO |
Bitrue |
Lien |
StrongBlock |
Auctus |
Finnexus |
DeFiDollar |
Celsius |
Synlev |
Perpetual Protocol |
Bifrost BiFi |
Hakka Finance |
Jarvis Network |
Although the list of sponsors is long. The addresses that actually deposited funds are not that many (there might be many reasons for this, that haven't been investigated).
Depositor Address | Total Token Amount |
---|---|
0x1b4c87add7a98d28910a0e1ab616d71aa4b4ce8b | 51000 |
0x27158157136384c713bc09a0a7ae81c8391d7f11 | 245000 |
0x2f0acb9c5dd2a3511bc1d9d67258e5c9434ba569 | 27500 |
0x6781a24ccc819941b7a4c9cc7b0dc32666e587e4 | 108500 |
0x71c05a4ea5e9d5b1ac87bf962a043f5265d4bdc8 | 132.3 |
The amount sponsors contributed has varied a bit through time.
Work has not been done to actually know how much each node has gotten. For that we would need to run every transaction that calls the withdraw function, and check the msg.sender
address (some could be internal transactions, so we would need to check every call
instruction done for every transaction).
Although to seen how much LINK token has been withdrawn from the escrow it is very straight forward.
Address Receiving Funds | Total withdrawn |
---|---|
0x039fdfdb14911608a34ebcda6009a80ef5d16e50 | 17631.3 |
0x0808d4689b347d499a96f139a5fc5b5101258406 | 2265.23 |
0x0a88fd0a0e8d99d70e051495d60b4eec3125175e | 207.03 |
0x0e5a997ba3b0e5a69e24468d30439911fe353961 | 1543.56 |
0x111f1b41f702c20707686769a4b7f25c56c533b2 | 15920.6 |
0x15918ff7f6c44592c81d999b442956b07d26cc44 | 18401.6 |
0x182d4990bb0ff595b308b3efcb93313abad575e7 | 15685.7 |
0x183a96629ff566e7aa8afa38980cd037eb40a59a | 17527.3 |
0x18bb60aee18a2ca3b082549fe65ec6f92598a6d2 | 121.55 |
0x210b4b57e4f1aa68a7e76e91d307fd4f88a7ab2d | 16604 |
0x3515d8cd82c021819f5d28cdec1a7ff702080676 | 1475.3 |
0x3651c260ee7100cbad162ca3bdb65da0d65cf0b7 | 201.6 |
0x3fb4600736d306ee2a89edf0356d4272fb095768 | 17386.9 |
0x4564a9c6061f6f1f2eadb954b1b3c241d2dc984e | 8406.67 |
0x4a3df8cae46765d33c2551ff5438a5c5fc44347c | 1075.73 |
0x4e64d4f804e66ed364d7456e0b49207fae94f9ac | 1390.88 |
0x4fbefaf1bff0130945c61603b97d38dd6e21f5cf | 18277.8 |
0x560e8bd3cb91cb00ab37c12e2280763483359381 | 378 |
0x61c808d82a3ac53231750dadc13c777b59310bd9 | 15182.7 |
0x7ac1ad6e4ba95783197038210a512ad697418c71 | 17923.6 |
0x8d689476eb446a1fb0065bffac32398ed7f89165 | 18241.3 |
0x95c98112bd9635a3159518401ae227d5a296e994 | 17205.7 |
0x9b18ed2d951151aaf71949885292f6f869785c7c | 4900.93 |
0x9ccbfd17fa284f36c2ff503546160b256d1cd3d1 | 17849.6 |
0x9d219125a0ce10241b4ec1280c2f880475f172f1 | 16066 |
0x9efa0a617c0552f1558c95993aa8b8a68b3e709c | 18354 |
0xab35418fb9f8b13e3e6857c36a0769b9f94a87ec | 17312.8 |
0xb54c7c9fa1a51300e6054b70ae9952c1fb2800b4 | 7829 |
0xb56a4dffe84f5a6edaa1971f80e8a7fb504c9edd | 1047.03 |
0xbdb624cd1051f687f116bb0c642330b2abdfcc06 | 17603.5 |
0xbf42c2ebc5e23661b255c92d8f7271e92668e795 | 15869.1 |
0xc8c30fa803833dd1fd6dbcdd91ed0b301eff87cf | 17854.9 |
0xdfbfb73f3013bc1584ccaa0cd2d9621194aed29b | 6803.79 |
0xfb71f6f2e0674a89cf60b734602226ad83495eb2 | 8.6 |
There isn't any obvious pattern from the outflows through time (x axis hidden due to having too many values).
This only covers superficially the research that can be done just by looking at the event emitted by the Chainlink contracts. I hope that it gives some clarity into some of the high level workings of the Chainlink price feed.