- Using the Witnet Celo Price Feed
In this tutorial, we will learn about how to integrate the Witnet Celo router and the CELO/USD price feed smart contract to fetch data about the currency pair that we will use in our example smart contract DExchange
where we will create a simple DEX(Decentralized Exchange) that allows users to trade their cUSD(Celo Dollar) tokens with CELO tokens stored in the exchange.
To get the most out of this tutorial, you need to have the following:
- Experience using Solidity.
- Familiarity with the most common smart contract concepts.
- Familiarity with the ERC-20 Token standard.
- Knowledge of interfaces.
- Experience using the Remix IDE.
- Experience using Laika for testing your smart contract.
- Understanding of oracles and their use cases.
- Experience using the Celo plugin to deploy smart contracts.
- A web browser
- An internet connection
- The Celo plugin activated in Remix
- The Remix IDE
- The Metamask Extension Wallet
- A Metamask account with Alfajores testnet cUSD and CELO tokens
In this section, we will create the DExchange
smart contract. To get started, open Remix and create a new file called DExchange.sol
.
To use the Witnet Celo router and the CELO/USD price feed smart contract, we will need to make a few imports:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "witnet-solidity-bridge/contracts/interfaces/IWitnetPriceRouter.sol";
import "witnet-solidity-bridge/contracts/interfaces/IWitnetPriceFeed.sol";
We first defined an SPDX license for our smart contract and the Solidity versions our compiler will be allowed to use. We then imported the IWitnetPriceRouter
and IwitnetPriceFeed
interfaces as we will need them in our smart contract to interact with the Witnet Celo Router and to fetch or force an update on the CELO/USD currency pair.
Next, we will also import the ERC-20 interface as we will need it to be able to interact with the cUSD smart contract:
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
We will now work on the variables and events we make use of in our smart contract:
contract DExchange {
IWitnetPriceRouter public immutable witnetPriceRouter;
IWitnetPriceFeed public celoUsdPrice;
address public owner;
address internal cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
event Exchange(address indexed sender, uint celoAmount, uint cUsdAmount);
}
We first defined our smart contract with the keyword contract
followed by the name DExchange
. Next, we created the following variables:
witnetPriceRouter
- An initialized interface that allows us to interact and call functions of the deployedWitnetPriceRouter
smart contractceloUsdPrice
- An initialized interface that allows us to interact and call functions of the deployed CELO/USD price feed smart contractowner
- Anaddress
variable that stores the address of the deployercUsdTokenAddress
- Anaddress
variable that stores the address of the cUSD smart contract
We also defined an event called Exchange
that will be emitted when a user successfully trades his cUSD tokens for CELO using the tradeCUsdToCelo()
function of our DExchange
smart contract.
In this section, we will explain what each function in our smart contract does.
We will now create the constructor of our smart contract:
/**
* IMPORTANT: use the Celo Alfajores WitnetPriceRouter address here.
* The link to the address is:
* https://docs.witnet.io/smart-contracts/witnet-data-feeds/addresses/celo
*/
constructor(IWitnetPriceRouter _router) {
witnetPriceRouter = _router;
updateCeloUsdPriceFeed();
owner = msg.sender;
}
The constructor is used to initialize the witnetPriceRouter
, celoUsdPrice
, and owner
variables.
Note: We initialize the
celoUsdPrice
variable by calling theupdateCeloUsdPriceFeed()
method.
Next, we will create the receive()
as it will be used for calls made to the contract with empty calldata, such as plain CELO transfers. This function will also play an important role in making sure that the forceCeloUsdUpdate()
function works properly.
receive() external payable {}
Note: The reason this function is important for the
forceCeloUSdUpdate()
function is because we make use of therequestUpdate()
method of the CELO/USD price feed smart contract which internally will send back unused funds in a plain transfer.
We will now create the updateCeloUsdPriceFeed()
function:
/**
* @notice Detects if the WitnetPriceRouter is now pointing to a different IWitnetPriceFeed implementation
* @notice Updates the celoUsdPrice with the new IWitnetPriceFeed implementation
*/
function updateCeloUsdPriceFeed() public {
IERC165 _newPriceFeed = witnetPriceRouter.getPriceFeed(
bytes4(0x9ed884be)
);
if (address(_newPriceFeed) != address(0)) {
celoUsdPrice = IWitnetPriceFeed(address(_newPriceFeed));
}
}
This function will be used to initialize(during deployment) and update the celoUsdPrice
variable that stores an interface for the ERC-165
compliant price feed smart contract of the CELO/ USD currency pair and this will allow us to use the interface to interact with the price feed smart contract. The updateCeloUsdPriceFeed()
first fetches and stores the ERC-165
price feed smart contract by passing the price pair identifier to the getPriceFeed()
method of the Witnet Celo Router that is used to fetch the price feed smart contract. Finally, we use an if
statement to make sure that we only update the celoUsdPrice
variable with an interface that is pointing to a valid and deployed smart contract.
Next up, we will create the getCeloUsdPrice()
function:
/// Returns the CELO / USD price (6 decimals), ultimately provided by the Witnet oracle, and
/// the timestamps at which the price was reported back from the Witnet oracle's sidechain
/// to Celo Alfajores.
function getCeloUsdPrice()
public
view
returns (int256 _lastPrice, uint256 _lastTimestamp)
{
(_lastPrice, _lastTimestamp, , ) = celoUsdPrice.lastValue();
}
The function is defined as public
and view
since we want to be able to use the function both internally and externally and only to read the state. It uses the lastValue()
method of the price feed smart contract stored in CeloUsdPrice
to fetch and return the following values:
_lastPrice
- Last valid price reported back from the Witnet oracle._lastTimestamp
- Last valid price reported back from the Witnet oracle.
We will now create the tradeCUsdToCelo()
function:
/**
* @notice Allow users to trade cUSD tokens for CELO tokens
* @dev The amount of cUSD to exchange is defined by the allowance set by the cUSD tokens' owner
*/
function tradeCUsdToCelo() public payable {
uint amount = IERC20Token(cUsdTokenAddress).allowance(
msg.sender,
address(this)
);
require(amount >= 0.1 ether);
(int _lastPrice, ) = getCeloUsdPrice();
uint celoAmount = (1 ether * amount) /
(uint(_lastPrice) * 0.000001 ether);
require(address(this).balance >= celoAmount);
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
address(this),
amount
),
"Transfer failed."
);
(bool success, ) = payable(msg.sender).call{value: celoAmount}("");
require(success, "Transfer failed");
emit Exchange(msg.sender, celoAmount, amount);
}
The tradeCUsdToCelo()
function allows users to trade their cUSD tokens for CELO tokens stored in the DExchange
smart contract. It fetches the cUSD allowance amount the sender has approved for the smart contract to "spend" and stores it in a variable called amount
. It then performs a check on the amount
variable to be at least one-tenth of a Celo Dollar. If nothing went wrong with the previous check, the function fetches and stores the latest valid price of the CELO/USD pair by calling the getCeloUsdPrice()
we defined earlier in our smart contract.
Note: You might have noticed that the value stored inside the
_lastPrice
variable uses six decimals. It follows the same principle of the eighteen decimals used by ERC-20 tokens which is simply to main good precision and accuracy as Solidity doesn't have afloat
type.
We then calculate and store the CELO amount the sender
will receive in the celoAmount
variable. The CELO amount is calculated by first multiplying the amount
variable by 1 ether
, then we convert the _lastPrice
to a uint
value as it was previously an int256
value. We then multiply it by 0.000001 ether
to convert it to 18 decimals digits and finally, we divide the results of both multiplication to get the CELO amount.
Note: We multiply the
amount
variable by1 ether
to ensure the value returned by the division operation is compliant with the CELO decimals which in this case is18
.
Next, we carry out a check to make sure that the DExchange
smart contract has enough CELO stored to fulfill the trade and we also carry out a check to make sure that the transfer of cUSD tokens to the DExchange
smart contract through the transferFrom()
method is successful.
Finally, we transfer the CELO amount to the sender using the .call()
method and ensure the transfer is successful. If nothing goes wrong, we emit the Exchange
event.
We will now define the withdrawCUsd()
function:
/// Allows the deployer to withdraw cUSD tokens
function withdrawCUsd() public payable {
require(msg.sender == owner);
uint amount = IERC20Token(cUsdTokenAddress).balanceOf(address(this));
require(amount > 0, "No cUSD balance to withdraw.");
require(
IERC20Token(cUsdTokenAddress).transfer(
msg.sender,
amount
),
"Transfer failed."
);
}
The withdrawCUsd()
function allows the owner
of the DExchange
smart contract to withdraw the cUSD tokens stored inside the smart contract. This function essentially checks whether the sender
of the transaction is the owner
. If it evaluates to true, the function fetches the current cUSD balance of the smart contract and ensures there is a valid amount to withdraw. Finally, it transfers the cUSD tokens using the transfer()
method of the cUSD smart contract.
The last function we will create for our smart contract is the forceCeloUsdUpdate()
function:
/// Force update on the CELO / USD currency pair
function forceCeloUsdUpdate() external payable {
IERC165 _priceFeed = witnetPriceRouter.getPriceFeed(bytes4(0x9ed884be));
uint _updateFee = IWitnetPriceFeed(address(_priceFeed))
.estimateUpdateFee(tx.gasprice);
IWitnetPriceFeed(address(_priceFeed)).requestUpdate{
value: _updateFee
}();
if (msg.value > _updateFee) {
payable(msg.sender).transfer(msg.value - _updateFee);
}
}
The forceCeloUsdUpdate()
function allows users to send a request to the CELO/USD price feed smart contract to update the currency pair's latest valid price. This function fetches the ERC-165
compliant price feed smart contract of the CELO/USD pair and then calls its estimateUpdateFee()
method to get the cost amount to create the request. The function then calls the requestUpdate()
method of the CELO/USD price feed smart contract and passes the _updateFee
retrieved to pay for the request. Finally, an if
statement checks if the msg.value
is greater than the _updateFee
, and if it is true, the unused CELO sent is sent back to the sender
of the transaction.
Here's the full code of our DExchange
smart contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "witnet-solidity-bridge/contracts/interfaces/IWitnetPriceRouter.sol";
import "witnet-solidity-bridge/contracts/interfaces/IWitnetPriceFeed.sol";
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
contract DExchange {
IWitnetPriceRouter public immutable witnetPriceRouter;
IWitnetPriceFeed public celoUsdPrice;
address public owner;
address internal cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
event Exchange(address indexed sender, uint celoAmount, uint cUsdAmount);
/**
* IMPORTANT: use the Celo Alfajores WitnetPriceRouter address here.
* The link to the address is:
* https://docs.witnet.io/smart-contracts/witnet-data-feeds/addresses/celo
*/
constructor(IWitnetPriceRouter _router) {
witnetPriceRouter = _router;
updateCeloUsdPriceFeed();
owner = msg.sender;
}
receive() external payable {}
/**
* @notice Detects if the WitnetPriceRouter is now pointing to a different IWitnetPriceFeed implementation
* @notice Updates the celoUsdPrice with the new IWitnetPriceFeed implementation
*/
function updateCeloUsdPriceFeed() public {
IERC165 _newPriceFeed = witnetPriceRouter.getPriceFeed(
bytes4(0x9ed884be)
);
if (address(_newPriceFeed) != address(0)) {
celoUsdPrice = IWitnetPriceFeed(address(_newPriceFeed));
}
}
/// Returns the CELO / USD price (6 decimals), ultimately provided by the Witnet oracle, and
/// the timestamps at which the price was reported back from the Witnet oracle's sidechain
/// to Celo Alfajores.
function getCeloUsdPrice()
public
view
returns (int256 _lastPrice, uint256 _lastTimestamp)
{
(_lastPrice, _lastTimestamp, , ) = celoUsdPrice.lastValue();
}
/**
* @notice Allow users to trade cUSD tokens for CELO tokens
* @dev The amount of cUSD to exchange is defined by the allowance set by the cUSD tokens' owner
*/
function tradeCUsdToCelo() public payable {
uint amount = IERC20Token(cUsdTokenAddress).allowance(
msg.sender,
address(this)
);
require(amount >= 0.1 ether);
(int _lastPrice, ) = getCeloUsdPrice();
uint celoAmount = (1 ether * amount) /
(uint(_lastPrice) * 0.000001 ether);
require(address(this).balance >= celoAmount);
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
address(this),
amount
),
"Transfer failed."
);
(bool success, ) = payable(msg.sender).call{value: celoAmount}("");
require(success, "Transfer failed");
emit Exchange(msg.sender, celoAmount, amount);
}
/// Allows the deployer to withdraw cUSD tokens
function withdrawCUsd() public payable {
require(msg.sender == owner);
uint amount = IERC20Token(cUsdTokenAddress).balanceOf(address(this));
require(amount > 0, "No cUSD balance to withdraw.");
require(
IERC20Token(cUsdTokenAddress).transfer(
msg.sender,
amount
),
"Transfer failed."
);
}
/// Force update on the CELO / USD currency pair
function forceCeloUsdUpdate() external payable {
IERC165 _priceFeed = witnetPriceRouter.getPriceFeed(bytes4(0x9ed884be));
uint _updateFee = IWitnetPriceFeed(address(_priceFeed))
.estimateUpdateFee(tx.gasprice);
IWitnetPriceFeed(address(_priceFeed)).requestUpdate{
value: _updateFee
}();
if (msg.value > _updateFee) {
payable(msg.sender).transfer(msg.value - _updateFee);
}
}
}
In this section, we will use Laika to test our smart contract to ensure everything works.
To get started, complete the following steps:
- Compile the
DExchange
smart contract - Deploy the smart contract using the Celo plugin in Remix
- Copy the address and ABI of the smart contract
- Go to Laika
- Click on the Connect button at the top right corner of your screen to connect your Metamask account
- Click on the New Request button found on the left side of your screen
- Select the New Request option
- Finally, paste your address and the ABI in the respective fields and click on the Import button
A new contract folder should appear under collections. It is an interface we can use to interact with our deployed smart contract.
The getCeloUsdPrice()
can easily be tested by calling the function and comparing the results with the latest valid price and timestamp being shown here.
Note: You can use the EpochConverter website to get the relative time from the
_lastTimestamp
. As for converting the_lastPrice
variable, you need to divide it by10 ** 6
.
The tradeCUsdToCelo()
can be tested by following the steps described:
-
Go to the cUSD smart contract page on the Celo Alfajores explorer using this link.
-
Copy the
DExchange
smart contract's address and use it as the argument forspender
when calling theapprove()
function. In this tutorial, we will setvalue
to one cUSD in wei.Note: The Metamask Wallet Extension should pop up and ask you to confirm the transaction. Ensure that the address connected is the one that will call the
tradeCUsdToCelo()
function. -
Next, go back to Laika and call the
getCeloUsdPrice()
price function and save the value for the_lastPrice
. -
Calculate and save the amount of CELO you should receive in
wei
for one cUSD by using this formula:CELO = (1 ether * value) / (_lastPrice * 0.000001 ether)
Note: Do not forget to replace
value
with one cUSD in wei,_lastPrice
with the value you fetched in the previous step, and theether
values in the formula with the respectivewei
amount. Finally, you can use the Desmos scientific calculator to calculate the amount. -
Call the
tradeCUsdToCelo()
on Laika and confirm the transaction. -
Copy the transaction hash, go to the Celo Alfajores Explorer, and paste it into the search field. In the Transaction Details, the CELO amount transferred should be approximately equal to the value saved in step four.
The withdrawCUsd()
can be tested by performing the following steps:
- Go to the cUSD smart contract page on the Celo Alfajores explorer using this link.
- Copy the
DExchange
smart contract's address and use it as the argument when calling thebalanceOf()
function of the Proxy. - Save the returned cUSD balance. We will need it later.
- Next, go back to Laika and call the
withdrawCUsd()
function of theDExchange
smart contract. - Go back to the cUSD smart contract page and call the
balanceOf()
function again on theDExchange
smart contract. - The smart contract's cUSD balance should now be zero.
- Finally, copy the transaction hash, go to the Celo Alfajores Explorer, and paste it into the search field. In the Transaction Details, the cUSD amount transferred should be approximately equal to the value saved in step three.
Note: The wallet address you use should be the address you used to deploy the
DExchange
smart contract. Calling thewithdrawCUsd()
with a different address will cause the transaction to revert.
To test the forceCeloUsdUpdate()
function in Laika, carry out the following process:
- Call the
getCeloUsdPrice()
function and save the results for comparison - Enter a reasonable value in the Transfer Value input field
- Next, call the
forceCeloUsdUpdate()
function - Ensure that the response does not return any errors
- Wait around ten minutes and then call the
getCeloUsdPrice()
- Notice that the timestamp and price are now different compared to the results you stored earlier
- Another way to confirm the update of the CELO/USD pair is by going to this page
In this tutorial, you learned how to implement and interact with the Witnet Celo Router and the CELO/USD price feed smart contract. Throughout the tutorial, we successfully built a simple DEX that allows us to trade cUSD tokens for CELO tokens.