Oracles provide a secure and reliable way for smart contracts to access real-time market data, which is essential for various DeFi applications.
API3 has developed an oracle node that is operated by the API Provider, removing the intermediary node layer, or middleman. This change in oracle architecture creates a scalable and transparent solution that enables first-party oracles to be aggregated according to user requirements. DeFi protocols looking to utilize first-party oracles do so by integrating dAPIs to their smart contracts.
With developers in mind, dAPIs have been designed with a simple integration process that abstracts away the technical implementation of accessing data feeds. The API3 Market provides the ability to manage these data feeds while giving users access to a range of first-party data feeds. In the future, additional data feed services, such as capturing Oracle Extracted Value (OEV) will be accessible once a dAPI has been integrated.
This tutorial will demonstrate how easy it is for developers to switch from Chainlink data feeds to API3’s first-party oracles.
To demonstrate how easy it is to port over from Chainlink to API3, we are going to port over the DeFi options contract from this Chainlink tutorial.
We don’t need to go over the entire contract because we will only be modifying a small section of the contract to port it over, but in summary, the contract enables users to create, buy, exercise, and cancel options within the contract using Ethereum (ETH) and Chainlink (LINK) tokens. It makes use of Chainlink’s aggregator interface for obtaining price feeds while exercising options. For more of an in-depth explanation, you can check out the full guide.
Now let’s look at the code we want to modify:
pragma solidity ^0.8.17;
import "https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.6/interfaces/LinkTokenInterface.sol";
import "https://github.com/smartcontractkit/chainlink/blob/master/evm-contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";
contract chainlinkOptions {
//Pricefeed interfaces
AggregatorV3Interface internal ethFeed;
AggregatorV3Interface internal linkFeed;
//Interface for LINK token functions
LinkTokenInterface internal LINK;
uint ethPrice;
uint linkPrice;
//Precomputing hash of strings
bytes32 ethHash = keccak256(abi.encodePacked("ETH"));
bytes32 linkHash = keccak256(abi.encodePacked("LINK"));
address payable contractAddr;
//Options stored in arrays of structs
struct option {
uint strike; //Price in USD (18 decimal places) option allows buyer to purchase tokens at
uint premium; //Fee in contract token that option writer charges
uint expiry; //Unix timestamp of expiration time
uint amount; //Amount of tokens the option contract is for
bool exercised; //Has option been exercised
bool canceled; //Has option been canceled
uint id; //Unique ID of option, also array index
uint latestCost; //Helper to show last updated cost to exercise
address payable writer; //Issuer of option
address payable buyer; //Buyer of option
}
option[] public ethOpts;
option[] public linkOpts;
//Kovan feeds: https://docs.chain.link/docs/reference-contracts
constructor() public {
//ETH/USD Kovan feed
ethFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
//LINK/USD Kovan feed
linkFeed = AggregatorV3Interface(0x396c5E36DD0a0F5a5D33dae44368D4193f69a1F0);
//LINK token address on Kovan
LINK = LinkTokenInterface(0xa36085F69e2889c224210F603D836748e7dC0088);
contractAddr = payable(address(this));
}
//Returns the latest ETH price
function getEthPrice() public view returns (uint) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = ethFeed.latestRoundData();
// If the round is not complete yet, timestamp is 0
require(timeStamp > 0, "Round not complete");
//Price should never be negative thus cast int to unit is ok
//Price is 8 decimal places and will require 1e10 correction later to 18 places
return uint(price);
}
//Returns the latest LINK price
function getLinkPrice() public view returns (uint) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = linkFeed.latestRoundData();
// If the round is not complete yet, timestamp is 0
require(timeStamp > 0, "Round not complete");
//Price should never be negative thus cast int to unit is ok
//Price is 8 decimal places and will require 1e10 correction later to 18 places
return uint(price);
}
//Updates prices to latest
function updatePrices() internal {
ethPrice = getEthPrice();
linkPrice = getLinkPrice();
}
-----------------------
-----------------------
}
The contract initializes the AggregatorV3Interfaces via the constructor for both the Eth and Link datafeeds; these can later be used to fetch the price. The two functions getEthPrice()
and getLinkPrice()
use the AggregatorV3Interface to fetch the price and return it. The options
struct defines how options are stored when they are created, along with the ethOpts
and linkOpts
array to store the created options.
Porting over the contract to use API3’s dAPIs in this options contract can be done in 3 easy steps:
getEthPrice()
and the getLinkPrice()
logic with a .read()
function call to the proxy interface
And that's it. You do not need to hold any special type of token to be able to read from the oracle, it is completely free to read.
Over 100+ dAPIs are currently available on the api3 market and work on a self-funded basis i.e you can top up the gas wallets to start reading from the oracle. If the dAPI is already funded you just need to copy the proxy address as seen below:
Here’s the same DeFi options contract updated to use API3 dAPIs
pragma solidity ^0.8.17;
import "@api3/contracts/v0.8/interfaces/IProxy.sol";
contract Api3Options {
//Pricefeed proxies
address public ethProxy;
address public linkProxy;
uint ethPrice;
uint linkPrice;
//Precomputing hash of strings
bytes32 ethHash = keccak256(abi.encodePacked("ETH"));
bytes32 linkHash = keccak256(abi.encodePacked("LINK"));
address payable contractAddr;
//Options stored in arrays of structs
struct option {
uint strike; //Price in USD (18 decimal places) option allows buyer to purchase tokens at
uint premium; //Fee in contract token that option writer charges
uint expiry; //Unix timestamp of expiration time
uint amount; //Amount of tokens the option contract is for
bool exercised; //Has option been exercised
bool canceled; //Has option been canceled
uint id; //Unique ID of option, also array index
uint latestCost; //Helper to show last updated cost to exercise
address payable writer; //Issuer of option
address payable buyer; //Buyer of option
}
option[] public ethOpts;
option[] public linkOpts;
//Kovan feeds: https://docs.chain.link/docs/reference-contracts
constructor(address _ethProxy, address _linkProxy) public {
//ETH/USD Proxy on Goerli
ethProxy = _ethProxy
//LINK/USD Proxy on Goerli
linkProxy = _linkProxy
contractAddr = payable(address(this));
}
//Returns the latest ETH price
function getEthPrice() public view returns (uint) {
(int224 value,uint32 timestamp) = IProxy(ethProxy).read();
// if the data feed is being updated with a one day-heartbeat
// interval, you may want to check for that.
require(
timestamp + 1 days > block.timestamp,
"Timestamp older than one day"
);
//Price should never be negative thus cast int to unit is ok
//Price is 18 decimal places
return uint(uint224(value));
}
//Returns the latest LINK price
function getLinkPrice() public view returns (uint) {
(int224 value,uint32 timestamp) = IProxy(linkProxy).read();
// if the data feed is being updated with a one day-heartbeat
// interval, you may want to check for that.
require(
timestamp + 1 days > block.timestamp,
"Timestamp older than one day"
);
//Price should never be negative thus cast int to unit is ok
//Price is 18 decimal places
return uint(uint224(value));
}
//Updates prices to latest
function updatePrices() internal {
ethPrice = getEthPrice();
linkPrice = getLinkPrice();
}
-----------------------
-----------------------
}
As you can see, we only needed to call .read()
on the IProxy(ethProxy)
interface to start reading from the datafeed. We ended up with lesser lines of code and a much simpler reading interface. You can try running the contract yourself on REMIX. (Note: The remix version only allows you to open and close options in ETH for simplicity).
dAPIs have been designed to abstract away the technical implementation of data feeds. Once a dAPI has been imported to oracle contracts, the API3 DAO can redirect the dAPI mapping upon user requests. This means that data feeds can be upgraded from self-funded to managed dAPIs, or directed to read alternate reference data with zero technical implementation.
Any update to data feeds, or a lack thereof, can create opportunities for OEV, such as arbitrage and liquidations. During each of these interactions value is leaking from the dApp users to both searchers and validators. Once a dAPI has been integrated, DeFi protocols will be able to capture Oracle Extractable Value without any further technical implementation.
Additionally, switching from Chainlink to API3 data feeds means no major alterations to smart contracts. This mitigates the need for audits while keeping battle-tested code intact.