- Using the Witnet Celo Price Feed
In this tutorial, we will use the Witnet Celo Price Feed router to fetch the CELO/USD currency pair that we will use in our smart contract DExchange
where we will allow users to trade their cUSD(Celo Dollar) tokens with CELO tokens stored in our smart contract.
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 and 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 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 {}
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 Price 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).transferFrom(
address(this),
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 transferFrom()
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 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.
Coming soon
In this tutorial, you learned how to implement and interact with the Witnet Celo Router and CELO/USD price feed smart contracts. Throughout the tutorial, we successfully built a simple DEX that allows us to trade cUSD tokens for CELO tokens.