Direct Request Coordinator 1.0.0 (DRCoordinator)
A framework that enables dynamic LINK payments on Direct Request (Any API), syncing the price with the network gas and token conditions. It targets node operators that seek being competitive on their Direct Request operations.
(New) Try it out with How To 01: DRCoordinator Basic Tutorial
See DRCoordinator 0.1.0 submission for the Chainlink Hackaton Spring 2022
Inspiration
The first version of DRCoordinator presented at the Spring 2022 hackaton was rushed and felt like a PoC (in fact there was lots of prototype testing!). Since then, I really wanted to revamp it with production standards and give it closure. With OCR2DR in the works the place of DRCoordinator on Direct Request production integrations is uncertain. Nevertheless, DRCoordinator 1.0.0 will help well anyone who wants to learn Web3 development on Ethereum with Solidity & Hardhat and few Chainlink products in depth (Direct Request and Price Feeds).
These are the new on-chain features (contracts):
- Adopted
fulfillData()
as fulfillment method instead offallback()
(which has been removed). - Standardised and improved custom errors and removed unused ones.
- Standardised and improved events.
- Added
Spec.paymentType
which enables REQUEST LINK payment as a percentage (as permiryad), apart from the flat payment type already supported. A percentage REQUEST LINK payment is more beneficial from the Operator point of view and allows settingminContractPaymentLinkJuels
as 0 Juels in all DRCoordinator TOML job specs (simplifying and standardising them all). - Added support for whitelisting consumers on-chain (authorised consumers) per
Spec
, as DRCoordinator TOML job specs must use therequesters
field to protect themselves from spamming attacks (due to lowminContractPaymentLinkJuels
). - Added a refund mode. If SPOT LINK payment is less than REQUEST LINK payment DRCoordinator refunds Consumer the difference.
- Added
consumerMaxPayment
, which allows Consumer to set a maximum LINK amount willing to pay per request. - Added multi Price Feed support (2-hop mode). DRCoordinator can calculate the wei units of GASTKN per unit of LINK (
weiPerUnitLink
) using two price feeds: GASTKN / TKN (priceFeed1
) and LINK / TKN (priceFeed2
). This mode allows to deploy DRCoordinator on networks where the LINK / GASTKN Price Feed is not available, e.g. Gnosis Chain, Moonriver, Moonbeam, Metis, etc. - Replaced the L2 Sequencer Offline Flag logic with L2 Sequencer Uptime Status Feeds to check L2 Sequencer availability on Arbitrum, Metis and Optimism.
- Added public lock in
DRCoordinator.sol
as per Read-only Reentrancy. It may be useful for Consumer devs as DRCoordinator has methods likeavailableFunds()
(which its result varies if read duringrequestData()
andfulfillData()
execution). - Added
permiryadFactor
, which allows tuning the fee percentage limits. - Improved interfaces and contracts inheritance.
- Simplified
DRCoordinator.cancelRequest()
by loadingFulfillConfig.expiration
andFulfillConfig.payment
. - Improved the Consumer libraries (contracts), e.g.
DRCoordinatorClient.sol
,ChainlinkExternalFulfillmentCompatible.sol
. - Removed
sha1
logic for syncing (CUD) JSON specs in DRCoordinator storage. - Removed
minConfirmations
logic after understanding that the Consumer plays no role on it (see Chainlink release v1.5.0 and Adjusting Minimum Outgoing Confirmations for high throughput jobs). Also that it is still not possible settingminConfirmations
from a job pipeline variable. - Applied Chainlink's Solidity Style Guide with few exceptions like not grouping by visibility, removing the leading
_
, or usingcallWithExactGas
(which I believe it is not needed thanks to theethtx
task). - Added NatSpec.
- Upgraded to Solidity v0.8.17.
These are the new off-chain features (Hardhat repository):
- Fixed bugs in utils, tasks, methods, etc.
- Improved transaction
overrides
(fromethers.js
) options. - Improved Web3 provider and signer management.
- Extended the network support (e.g. Optimism / Arbitrum Goerli, Klaytn Baobab, etc.) with regards to the Chainlink framework (e.g. LinkToken, Price Feeds), contract deployment & verification, etc.
- Improved test suite and added GitHub Actions CI.
- Improved project folder structure.
- Improved tasks documentation.
- Updated dependencies.
How it works
This is a high level overview of the Direct Request Model with DRCoordinator:
1. Deploying a DRCoordinator
NodeOps have to deploy and set up first a DRCoordinator:
- Deploy, set up and verify a DRCoordinator using the
drcoordinator:deploy
Hardhat task.- NB: By default it will attempt to fetch the LINK / TKN Price Feed on the network and it will error if it is not found. In this case NodeOps will require to deploy in Multi Price Feed mode (See Price Feed Contract Addresses for choosing the right Price Feeds).
- Amend any non-immutable config after deployment using the
drcoordinator:set-config
Hardhat task. - NodeOps can check the DRCoordiantor storage detail using the
drcoordinator:detail
Hardhat task.
2. Adding the job on the Chainlink node
NodeOps have to add a DRCoordinator-friendly TOML job spec (image no 1), which only requires to:
- Set the
minContractPaymentLinkJuels
field to 0 Juels. Make sure to set first the node env varMINIMUM_CONTRACT_PAYMENT_LINK_JUELS
to 0 as well. - Add the DRCoordinator address in
requesters
to prevent the job being spammed (due to 0 Juels payment). - Add an extra data encode as
(bytes32 requestId, bytes data)
(viaethabiencode
orethabiencode2
tasks) before encoding the data for thefulfillOracleRequest2
tx.
3. Making the job requestable
NodeOps have to:
- Create the
Spec
(seeSpecLibrary.sol
) of the TOML spec added above (image no 2 & 3) and upload it in the DRCoordinator storage viaDRCoordinator.setSpec()
(image no 4).
- NodeOps should create the equivalent JSON Spec and upload it using the
drcoordinator:import-file
Hardhat task.
- Use
DRCoordinator.addSpecAuthorizedConsumers()
if on-chain whitelisting of consumers is desired. - Share/communicate the
Spec
details (via its key) so the Consumer devs can monitor theSpec
and act upon any change on it, e.g.fee
,payment
, etc.
4. Implementing the Consumer
Devs have to:
- Make Consumer inherit from
DRCoordinatorClient.sol
(an equivalent ofChainlinkClient.sol
for DRCoordinator requests). This library only builds theChainlink.Request
and then sends it to DRCoordinator (viaDRCoordinator.requestData()
), which is responsible for extending it and ultimately sending it to Operator. - Request a
Spec
by passing the Operator address, the maximum amount of gas willing to spend, the maximum amount of LINK willing to pay and theChainlink.Request
(which includes theSpec.specId
asid
and the request parameters CBOR encoded) (image no 5).
Devs can time the request with any of these strategies if gas prices are a concern:
- Call
DRCoordinator.calculateMaxPaymentAmount()
. - Call
DRCoordinator.calculateSpotPaymentAmount()
. - Call
DRCoordinator.getFeedData()
.
5. Requesting the job spec
NB: Make sure Consumer has LINK balance in DRCoordinator.
When Consumer calls DRCoordinator.requestData()
DRCoordinator does (image no 5):
- Validates the arguments.
- Calculates MAX LINK payment amount, which is the amount of LINK Consumer would pay if all the
callbackGasLimit
was used fulfilling the request (txgasLimit
) (image no 6). - Checks that the Consumer balance can afford MAX LINK payment and that Consumer is willing to pay the amount.
- Calculates the LINK payment amount (REQUEST LINK payment) to be hold in escrow by Operator. The payment can be either a flat amount or a percentage (permiryad) of MAX LINK payment. The
paymentType
andpayment
are set in theSpec
by NodeOp. - Updates Consumer balancee.
- Stores essential data from Consumer,
Chainlink.Request
andSpec
in aFulfillConfig
(by request ID) struct to be used upon fulfillment. - Extends the Consumer
Chainlink.Request
and sends it to Operator (paying the REQUEST LINK amount) (image no 7), which emits theOracleRequest
event (image no 8).
6. Requesting the Data Provider(s) API(s), processing the response(s) and submitting the result on-chain
NB: all these steps follow the standard Chainlink Direct Request Model.
- The Chainlink node subscribed to the event triggers a
directrequest
job run. - The
OracleRequest
event data is decoded and the log and request parameters are processed and (9) used to request the Data Povider(s) API(s) (image no 9). - The API(s) response(s) (image no 10) are processed and the result is submitted on-chain back to DRCoordinator via
Operator.fulfillOracleRequest2()
(image no 11 & 12).
- NB: forwarding the response twice (i.e. Operator -> DRCoordinator -> Consumer) requires to encode the result as
bytes
twice (viaethabiencode
orethabiencode2
)./ - NB: the
gasLimit
parameter of theethtx
task has set the amount defined by Consumer when calledDRCoordinator.requestData()
plusGAS_AFTER_PAYMENT_CALCULATION
(50_000
gas units).
7. Fulfilling the request
- Validates the request and its caller.
- Loads the request configuration (
FulfillConfig
) and attempts to fulfill the request by calling the Consumer callback method passing the response data (image no 13 & 14). - Calculates SPOT LINK payment, which is the equivalent gas amount used fulfilling the request in LINK, minus the REQUEST LINK payment, plus the fulfillment fee (image no 15). The fee can be either a flat amount of a percentage (permiryad) of SPOT LINK payment. The
feeType
andfee
are set in theSpec
by NodeOp. - Checks that the Consumer balance can afford SPOT LINK payment and that Consumer is willing to pay the amount. It is worth mentioning that DRCoordinator can refund Consumer if REQUEST LINK payment was greater than SPOT LINK payment and DRCoordinator's balance is greater or equal than SPOT payment. Tuning the
Spec.payment
andSpec.fee
should make this particular case very rare. - Updates Consumer and DRCoordinator balances.
Challenges I ran into
The current DRCoordinator design makes it work as a "forwarder", as it forwards requests and responses between the Consumer & Operator contracts. I call this design DRCoordinator-Cooperator. The first task I addressed for this hackaton was prototyping a DRCoordinator-Operator, which would have been a DRCoordinator that inherited from Operator and extended its functionality for a new kind of requests using any of these approaches:
- Implement internal LINK balances (do not use
LINK.transferAndCall()
) and just emit anOracleRequest
event (which is required to triggerdirectrequest
jobs, but it does not require to be preceded by a LINK payment). - Use
LINK.transferAndCall()
and implement a newDRCOORDINATOR_REQUEST_SELECTOR
.
None of them came to fruition due to the Operator.sol
subclassing limitations; the key methods have private
visibility. Despite either modifying Operator.sol
or getting rid of it was an option, I wanted to stick to the Chainlink standards and do not increase the risk of NodeOps (Operator.sol
has been audited & widely tested).
Accomplishments that I'm proud of
Having the willpower of revamping the version presented at the previous Chainlink Hackaton knowing OCR2DR is in the works. DRCoordinator 1.0.0 is a more mature product and I've achieved many of the "What's Next?" bullet points listed on the previous hackaton.
Also the multi Price Feed mode (2-hops mode), and running lots of experiments with directrequest
jobs trying the new built-in core tasks, e.g. ethabiencode2
.
What's next for DRCoordinator
Contract improvements:
- Factor in the L1 fees when having to calculate MAX / SPOT LINK payment amount on L2s. The Chainlink KeeperRegistryBase1_3 does it for Arbitrum and Optimism.
- Fuzz testing.
- Consider extending
Spec
(or a related mapping) with job spec metadata, e.g. an optional IPFS CID that points to the integration docs. - See if there is a way to improve the Etherscan experience without sacrificing the contracts design.
DRCoordinator.sol
NatSpec is not well displayed (is it because of inheriting it from the interfaces?) and custom errors revert reasons are not well displayed either.
Tasks & tools improvements:
- Add any missing task and/or tool based on users' feedback, for instance "add funds".
- Incorporate some of the DRAFT tasks & concepts that help troubleshooting requests, e.g. decoding fulfillment transactions.
- Improve the README.md and How To guides. For instance include examples of different DRCoordinator configs (e.g. multi price feed, L2 Sequencer Uptime), TOML/JSON specs, showcase the refund mode, etc.
- Keep track of all the above introducing a changelog.
But also:
- Support any NodeOp that wants to give it a try. For instance an improved
ethabiencode2
task would allow NodeOps not having to list, price & manage N Get jobs (e.g.(uint256)
,(uint256,uint256)
,(uint256, uint256, uint256)
, ...) cause each Consumer could enforce its desired ABI per request. - Try to understand DRCoordinator's place in the ecosystem once OCR2DR is released.
What I learned
An even deeper dive into Chainlink Direct Request.
Built With
- chainlink
- hardhat
- infura
- solidity
- toml
- typescript
Log in or sign up for Devpost to join the conversation.