Price Feeds
Price feeds enabled by an oracle serve as a bridge between real-world data feed and the blockchain. They provide real time pricing data that is aggregated from multiple trusted external sources ( e.g. crypto exchanges, financial data providers, etc. ) to the blockchain network.
For the example in this book section, we will use Pragma Oracle to read the
price feed for ETH/USD asset pair and also showcase a mini application that
utilizes this feed.
Pragma Oracle is a leading zero knowledge oracle that provides access to off-chain data on Starknet blockchain in a verifiable way.
Setting Up Your Contract for Price Feeds
Add Pragma as a Project Dependency
To get started with integrating Pragma on your Cairo smart contract for price
feed data, edit your project's Scarb.toml file to include the path to use
Pragma.
[dependencies]
pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" }
Creating a Price Feed Contract
After adding the required dependencies for your project, you'll need to define a contract interface that includes the required pragma price feed entry point.
#[starknet::interface]
pub trait IPriceFeedExample<TContractState> {
fn buy_item(ref self: TContractState);
fn get_asset_price(self: @TContractState, asset_id: felt252) -> u128;
}
Of the two public functions exposed in the IPriceFeedExample, the one
necessary to interact with the pragma price feed oracle is the get_asset_price
function, a view function that takes in the asset_id argument and returns a
u128 value.
Import Pragma Dependencies
use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait};
use pragma_lib::types::{DataType, PragmaPricesResponse};
use starknet::contract_address::contract_address_const;
use starknet::get_caller_address;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use super::{ContractAddress, IPriceFeedExample};
const ETH_USD: felt252 = 19514442401534788;
const EIGHT_DECIMAL_FACTOR: u256 = 100000000;
#[storage]
struct Storage {
pragma_contract: ContractAddress,
product_price_in_usd: u256,
}
#[constructor]
fn constructor(ref self: ContractState, pragma_contract: ContractAddress) {
self.pragma_contract.write(pragma_contract);
self.product_price_in_usd.write(100);
}
#[abi(embed_v0)]
impl PriceFeedExampleImpl of IPriceFeedExample<ContractState> {
fn buy_item(ref self: ContractState) {
let caller_address = get_caller_address();
let eth_price = self.get_asset_price(ETH_USD).into();
let product_price = self.product_price_in_usd.read();
// Calculate the amount of ETH needed
let eth_needed = product_price * EIGHT_DECIMAL_FACTOR / eth_price;
let eth_dispatcher = ERC20ABIDispatcher {
contract_address: contract_address_const::<
0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7,
>() // ETH Contract Address
};
// Transfer the ETH to the caller
eth_dispatcher
.transfer_from(
caller_address,
contract_address_const::<
0x0237726d12d3c7581156e141c1b132f2db9acf788296a0e6e4e9d0ef27d092a2,
>(),
eth_needed,
);
}
fn get_asset_price(self: @ContractState, asset_id: felt252) -> u128 {
// Retrieve the oracle dispatcher
let oracle_dispatcher = IPragmaABIDispatcher {
contract_address: self.pragma_contract.read(),
};
// Call the Oracle contract, for a spot entry
let output: PragmaPricesResponse = oracle_dispatcher
.get_data_median(DataType::SpotEntry(asset_id));
return output.price;
}
}
}
The snippet above shows the necessary imports you need to add to your contract module in order to interact with the Pragma oracle.
Required Price Feed Function Impl in Contract
fn get_asset_price(self: @ContractState, asset_id: felt252) -> u128 {
// Retrieve the oracle dispatcher
let oracle_dispatcher = IPragmaABIDispatcher {
contract_address: self.pragma_contract.read(),
};
// Call the Oracle contract, for a spot entry
let output: PragmaPricesResponse = oracle_dispatcher
.get_data_median(DataType::SpotEntry(asset_id));
return output.price;
}
The get_asset_price function is responsible for retrieving the price of the
asset specified by the asset_id argument from Pragma Oracle. The
get_data_median method is called from the IPragmaDispatcher instance by
passing the DataType::SpotEntry(asset_id) as an argument and its output is
assigned to a variable named output of type PragmaPricesResponse. Finally,
the function returns the price of the requested asset as a u128.
Example Application Using Pragma Price Feed
#[starknet::contract]
mod PriceFeedExample {
use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait};
use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait};
use pragma_lib::types::{DataType, PragmaPricesResponse};
use starknet::contract_address::contract_address_const;
use starknet::get_caller_address;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use super::{ContractAddress, IPriceFeedExample};
const ETH_USD: felt252 = 19514442401534788;
const EIGHT_DECIMAL_FACTOR: u256 = 100000000;
#[storage]
struct Storage {
pragma_contract: ContractAddress,
product_price_in_usd: u256,
}
#[constructor]
fn constructor(ref self: ContractState, pragma_contract: ContractAddress) {
self.pragma_contract.write(pragma_contract);
self.product_price_in_usd.write(100);
}
#[abi(embed_v0)]
impl PriceFeedExampleImpl of IPriceFeedExample<ContractState> {
fn buy_item(ref self: ContractState) {
let caller_address = get_caller_address();
let eth_price = self.get_asset_price(ETH_USD).into();
let product_price = self.product_price_in_usd.read();
// Calculate the amount of ETH needed
let eth_needed = product_price * EIGHT_DECIMAL_FACTOR / eth_price;
let eth_dispatcher = ERC20ABIDispatcher {
contract_address: contract_address_const::<
0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7,
>() // ETH Contract Address
};
// Transfer the ETH to the caller
eth_dispatcher
.transfer_from(
caller_address,
contract_address_const::<
0x0237726d12d3c7581156e141c1b132f2db9acf788296a0e6e4e9d0ef27d092a2,
>(),
eth_needed,
);
}
fn get_asset_price(self: @ContractState, asset_id: felt252) -> u128 {
// Retrieve the oracle dispatcher
let oracle_dispatcher = IPragmaABIDispatcher {
contract_address: self.pragma_contract.read(),
};
// Call the Oracle contract, for a spot entry
let output: PragmaPricesResponse = oracle_dispatcher
.get_data_median(DataType::SpotEntry(asset_id));
return output.price;
}
}
}
Note: Pragma returns the value of different token pairs using the decimal factor of 6 or 8. You can convert the value to the required decimal factor by dividing the value by \( {10^{n}} \), where
nis the decimal factor.
The code above is an example implementation of an applications consuming a price
feed from the Pragma oracle. The contract imports necessary modules and
interfaces, including the IPragmaABIDispatcher for interacting with the Pragma
oracle contract and the ERC20ABIDispatcher for interacting with the ETH ERC20
token contract.
The contract has a const that stores the token pair ID of ETH/USD, and a
Storage struct that holds two fields pragma_contract and
product_price_in_usd. The constructor function initializes the
pragma_contract address and sets the product_price_in_usd to 100.
The buy_item function is the main entry point for a user to purchase an item.
It retrieves the caller's address. It calls the get_asset_price function to
get the current price of ETH in USD using the ETH_USD asset ID. It calculates
the amount of ETH needed to buy the product based on the product price in USD at
the corresponding ETH price. It then checks if the caller has enough ETH by
calling the balance_of method on the ERC20 ETH contract. If the caller has
enough ETH, it calls the transfer_from method of the eth_dispatcher instance
to transfer the required amount of ETH from the caller to another contract
address.
The get_asset_price function is the entry point to interact with the Pragma
oracle and has been explained in the section above.
You can get a detailed guide on consuming data using Pragma price feeds on their documentation.