Launching Price Feeds with Phala¶
Introduction¶
Phala Network is an off-chain compute network powered by Secure Enclaves that enables developers to build powerful smart contracts that connect to off-chain components called Phat Contracts. Phat Contracts are designed to enable functionality that surpasses the limitations of traditional smart contracts, such as storage, cost, and compute limitations while remaining trustless, verifiable, and permissionless. For more information about Phala's architecture, be sure to check out the Phala docs.
Phala is not an oracle network itself; rather, Phala enables a variety of off-chain compute capabilities, such as a decentralized oracle network. Phala also provides a toolset called Phala Bricks that makes it easy to quickly launch these types of features without having to build them from scratch.
This tutorial will walk through a demo of interacting with price feeds enabled by Phat contracts on the demo Tanssi EVM-compatible appchain. Next, you'll learn how to deploy price feeds to your Tanssi EVM-compatible appchain. Please be advised that the steps shown in this tutorial are for demonstration purposes only - it's highly recommended that you contact the Phala team directly as they can assist you with launching price feeds on an appchain to ensure the integrity of the deployment process.
How Phala Enables Price Feeds¶
Phala mirrors Chainlink Price Feeds from Ethereum MainNet. Chainlink Price Feeds have stood the test of time and have wide industry adoption. As a reminder, Chainlink Price Feeds don't rely on any single source of truth, rather, their pricing data is collected and aggregated from a variety of data sources gathered by a decentralized set of independent node operators. This helps to prevent manipulation and erratic pricing data.
The core component of Phala's system design is the Secure Enclave, which processes the inputs it receives from the Phala blockchain, acting as an encrypted message queue, and guarantees secure and faithful execution, regardless of the presence of malicious workers. In this sense, the Phala blockchain requests a price feed update, which the Phala off-chain workers fetch from Ethereum MainNet, and return to the Phala blockchain.
It's important to note that Phala isn't limited to replicating existing Oracles. You can create entirely new Oracles by sourcing off-chain data via Phat Contracts. In this Phat-EVM Oracle example, pricing data is sourced from the CoinGecko API. Price quote updates can then be constantly streamed from the Phat contract (push design), or the EVM smart contract can ask for a refreshed quote from the Phat contract (pull design).
Fetch Price Data¶
There are several price feeds available on the demo EVM appchain that you can interact with. The price feeds enabled by Phat Contracts use the same interface as the Chainlink price feeds. The data lives in a series of smart contracts (one per price feed) and can be fetched with the aggregator interface:
AggregatorV3Interface.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface AggregatorV3Interface {
/**
* Returns the decimals to offset on the getLatestPrice call
*/
function decimals() external view returns (uint8);
/**
* Returns the description of the underlying price feed aggregator
*/
function description() external view returns (string memory);
/**
* Returns the version number representing the type of aggregator the proxy points to
*/
function version() external view returns (uint256);
/**
* Returns price data about a specific round
*/
function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
/**
* Returns price data from the latest round
*/
function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
As seen above in the interface, there are five functions for fetching data: decimals
, description
, version
, getRoundData
, and latestRoundData
. For more information about the AggregatorV3Interface.sol
, see the Chainlink API Reference.
Supported Assets¶
Phala sources its price feed data by mirroring Chainlink's price feeds from Ethereum MainNet. Currently, there are data feed contracts for the demo EVM appchain for the following asset pairs:
Asset & Base Pair | Aggregator Contract |
---|---|
AAVE to USD | 0x2E1640853bB2dD9f47831582665477865F9240DB |
BTC to USD | 0x89BC5048d634859aef743fF2152363c0e83a6a49 |
CRV to USD | 0xf38b25b79A72393Fca2Af88cf948D98c64726273 |
DAI to USD | 0x1f56d8c7D72CE2210Ef340E00119CDac2b05449B |
ETH to USD | 0x739d71fC66397a28B3A3b7d40eeB865CA05f0185 |
USDC to USD | 0x4b8331Ce5Ae6cd33bE669c10Ded9AeBA774Bf252 |
USDT to USD | 0x5018c16707500D2C89a0446C08f347A024f55AE3 |
Asset & Base Pair | Aggregator Contract |
---|---|
AAVE to USD | 0x547a514d5e3769680Ce22B2361c10Ea13619e8a9 |
BTC to USD | 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c |
CRV to USD | 0xCd627aA160A6fA45Eb793D19Ef54f5062F20f33f |
DAI to USD | 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9 |
ETH to USD | 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419 |
USDC to USD | 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 |
USDT to USD | 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D |
Interacting with Price Feeds on the Tanssi Demo EVM Appchain¶
Next, this tutorial will showcase interacting with the price feed contracts on the demo EVM appchain. These contracts are already deployed on the demo EVM appchain, so you can interact with them by accessing the aggregator contract corresponding to your desired asset.
For a refresher on setting up Remix to interface with the demo EVM appchain, see the Deploy Smart Contracts with Remix guide. Secondly, make sure you have connected MetaMask to the demo EVM appchain.
Paste the aggregator contract into a new file in Remix and compile it.
Then, take the following steps:
- Head to the Deploy and Run Transactions tab
- Set the ENVIRONMENT to Injected Provider -- MetaMask
- Select the AggregatorV3Interface contract from the CONTRACT dropdown
- Enter the data feed contract address corresponding to
BTC to USD
, which is0x89BC5048d634859aef743fF2152363c0e83a6a49
on the demo EVM appchain in the At Address field and click the At Address button
The aggregator contract should now be accessible. To interact with the aggregator contract, take the following steps:
- Expand the AggregatorV3Interface contract to reveal the available functions
- Click decimals to query how many digits after the decimal point are included in the returned price data
- Click description to verify the asset pair of the price feed
- Click latestRoundData to see the most recent price data for the asset pair. The price data for the pair is returned as the int256 answer
Note that to obtain a readable price, you must account for the decimals of the price feed, which is available with the decimals()
method. So in this example, where the price feed returned a value of 5230364122303
, the decimal point will need to be moved eight places, which corresponds to a Bitcoin price of $52,303.64
at the time of writing.
Launching Price Feeds on an EVM Appchain¶
It's easy to launch price feeds on a Tanssi EVM appchain! The following sections will walk through the process of launching a variety of price feeds. This process can be followed for Snap appchains and dedicated appchains on the Tanssi Dancebox TestNet. Please be advised that these instructions are for demonstration purposes only, and it's highly recommended that you contact the Phala Team for assistance in any production scenarios.
Setup¶
To get started, clone the Phala Mirrored Price Feed repo to a local directory. Then, run the following command:
cd mirrored-price-feed/ && yarn install
Then, you'll need to configure your .env
file. There's a convenient sample file in the repo that you can refer to. From the command line, run:
cp env.example .env
Next, edit your .env
to insert the private key of an account funded on your appchain, and the RPC URL of your appchain. If building on your own appchain, you can fund a dummy account from the Sudo account of your appchain. Your appchain's Sudo address and RPC URL are both accessible from your dashboard on the Tanssi DApp. You can leave the other fields in the .env
blank. Your .env
should resemble the below:
PRIVATE_KEY=INSERT_PRIVATE_KEY
RPC_URL=INSERT_YOUR_APPCHAIN_RPC_URL
VERIFIER_URL=
VERIFY_ADDRESS=
Note
You should never share your seed phrase (mnemonic) or private key with anyone. This gives them direct access to your funds. This guide is for educational purposes only.
Configure Deployment Script¶
Next, you'll need to edit the OffchainAggregator.s.sol
file located in the scripts directory. OffchainAggregator.sol
takes two parameters upon deployment, a decimals
value, and a description of the price feed. The decimal value can remain unchanged at 8
, and the description should be changed to the price feed that you'd like to add to your appchain. In this case, BTC / USD
is specified. Take care to copy the description exactly as shown, and remember that only specified assets shown in the Fetch Price Feed Data section are supported. If you specify an asset not supported by Phala, the price feed will not work correctly. Your OffchainAggregator.s.sol
should resemble the following:
OffchainAggregator.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console2} from "forge-std/Script.sol";
import {OffchainAggregator} from "../src/OffchainAggregator.sol";
contract OffchainAggregatorScript is Script {
function setUp() public {}
function run() public {
vm.startBroadcast();
OffchainAggregator aggregator = new OffchainAggregator(
8,
'BTC / USD'
);
console2.log(address(aggregator));
vm.stopBroadcast();
}
}
There are a few more changes that you need to make in feeder.ts
, the file that maintains and updates your price feeds. You'll need to insert the details of your EVM appchain as follows:
const chain = defineChain({
id: INSERT_EVM_CHAIN_ID,
name: 'dancebox-evm-appchain',
rpcUrls: {
default: {
http: ['INSERT_RPC_URL'],
},
public: {
http: ['INSERT_RPC_URL'],
},
},
});
You'll also see two arrays of contract addresses at the top of feeder.ts
. The first array, named mainnetFeedContracts
refers to Ethereum MainNet aggregator contract addresses, and you can leave that untouched. The second array, named aggregatorContracts
still contains the addresses of the aggregator contracts on the demo EVM appchain. You should erase this array such that it is empty. Later in this guide, you'll return to it and add the contract addresses of your aggregator contracts specific to your Tanssi EVM appchain once they are deployed.
Once you're finished editing, your feeder.ts
file should resemble the below:
feeder.ts
import {
createPublicClient,
http,
parseAbi,
createWalletClient,
defineChain,
} from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import * as dotenv from 'dotenv';
dotenv.config();
const mainnetFeedContracts = {
'AAVE-USD': '0x547a514d5e3769680Ce22B2361c10Ea13619e8a9',
'CRV-USD': '0xCd627aA160A6fA45Eb793D19Ef54f5062F20f33f',
'ETH-USD': '0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419',
'BTC-USD': '0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c',
'DAI-USD': '0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9',
'USDT-USD': '0x3E7d1eAB13ad0104d2750B8863b489D65364e32D',
'USDC-USD': '0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6',
};
const aggregatorContracts = {};
const abi = parseAbi([
'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
'function transmit(uint80 _roundId, int192 _answer, uint64 _timestamp) external',
'function getRoundData(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
]);
// Insert your appchain details here
const chain = defineChain({
id: INSERT_EVM_CHAIN_ID,
name: 'dancebox-evm-appchain',
rpcUrls: {
default: {
http: ['INSERT_RPC_URL'],
},
public: {
http: ['INSERT_RPC_URL'],
},
},
});
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const targetChainPublicClient = createPublicClient({
chain,
transport: http(),
});
async function getLatestRoundData(pair: string) {
const address = mainnetFeedContracts[pair];
if (!address) {
throw new Error(`${pair} mainnet feed contract did not exist.`);
}
const data = await publicClient.readContract({
address,
abi,
functionName: 'latestRoundData',
});
return data;
}
async function getRoundDataFromAggregator(pair: string, roundId: number) {
const address = aggregatorContracts[pair];
if (!address) {
throw new Error(`${pair} aggregator contract did not exist.`);
}
const publicClient = createPublicClient({
chain,
transport: http(),
});
try {
const data = await publicClient.readContract({
address,
abi,
functionName: 'getRoundData',
args: [roundId],
});
return data;
} catch {}
}
async function updateFeed(
walletClient: ReturnType<createWalletClient>,
pair: string
) {
if (!aggregatorContracts[pair]) {
throw new Error(`${pair} aggregator contract did not exist.`);
}
const [roundId, answer, startedAt, updatedAt, answeredInRound] =
await getLatestRoundData(pair);
const aggregatorRoundId = Number(roundId & BigInt('0xFFFFFFFFFFFFFFFF'));
const data = await getRoundDataFromAggregator(pair, aggregatorRoundId);
if (data[1] === answer) {
console.info(
`${pair} aggregatorRoundId ${aggregatorRoundId} data exists: ${data}`
);
return;
}
const hash = await walletClient.writeContract({
address: aggregatorContracts[pair],
abi,
functionName: 'transmit',
args: [roundId, answer, startedAt],
});
await targetChainPublicClient.waitForTransactionReceipt({ hash });
console.info(`${pair} updated, transmit tx hash: ${hash}`);
}
async function main() {
if (!process.env.PRIVATE_KEY) {
throw new Error('missing process.env.PRIVATE_KEY');
}
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
chain,
transport: http(),
account,
});
for (const pair in aggregatorContracts) {
await updateFeed(walletClient, pair);
}
}
main()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});
Build and Test¶
Run the following commands to build and test the project:
yarn build
yarn test
If everything was successful, you'll see output like the following:
Deploy¶
To deploy your aggregator contract for the specified asset/base pair to your EVM appchain, use the following command:
yarn deploy
You'll get a transaction status as well as a contract address. Copy this contract address, as you'll need to refer to it in the following steps.
Access Aggregator Contract¶
Next, this tutorial will demonstrate interacting with the newly deployed aggregator contract. Make sure that your MetaMask wallet is connected to your EVM appchain. You can add your appchain to your MetaMask by pressing Add to MetaMask on your dashboard on the Tanssi DApp.
Paste the aggregator contract into a new file in Remix and compile it.
Then, take the following steps:
- Head to the Deploy and Run Transactions tab
- Set the ENVIRONMENT to Injected Provider -- MetaMask and ensure that your MetaMask is on the network of your EVM appchain. You can verify the EVM chain ID in Remix if you are unsure
- Select the AggregatorV3Interface contract from the CONTRACT dropdown
- Enter the data feed contract address corresponding to your desired asset pair that was returned on the command line in the prior section in the At Address field and click the At Address button
Expand the AggregatorV3Interface contract to reveal the available functions and click latestRoundData to see the most recent price data for the asset pair. You should see 0
values for all. This is because your aggregator contract has been deployed, but it hasn't yet fetched price data. You can fix this with a quick price feed update.
Trigger Price Feed Update¶
In a prior section, you cleared out the array of aggregator contracts, but since you've now deployed an aggregator contract, you should specify it in the feeder.ts
file so that you can manually trigger a refresh of the price data. Edit the aggregatorContracts
array as follows:
const aggregatorContracts = {
'BTC-USD': 'INSERT_AGGREGATOR_CONTRACT_ADDRESS',
}
Then, from the command line, run the following command:
npx tsx feeder.ts
Upon returning to Remix, click latestRoundData once more, and after waiting a moment, you should see an accurate value returned.
For more information about using Phala to access off-chain data, be sure to check out the Phala docs site.
| Created: February 15, 2024