Skip to content

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:

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.

Compile aggregator contract

Then, take the following steps:

  1. Head to the Deploy and Run Transactions tab
  2. Set the ENVIRONMENT to Injected Provider -- MetaMask
  3. Select the AggregatorV3Interface contract from the CONTRACT dropdown
  4. Enter the data feed contract address corresponding to BTC to USD, which is 0x89BC5048d634859aef743fF2152363c0e83a6a49 on the demo EVM appchain in the At Address field and click the At Address button

Access aggregator contract

The aggregator contract should now be accessible. To interact with the aggregator contract, take the following steps:

  1. Expand the AggregatorV3Interface contract to reveal the available functions
  2. Click decimals to query how many digits after the decimal point are included in the returned price data
  3. Click description to verify the asset pair of the price feed
  4. 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

Check price data

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:

yarn build yarn run v1.22.10 forge build [.] compiling No files changed, compilation skipped ✨ Done in 0.765. yarn test yarn run v1.22.10 forge test [.] compiling No files changed, compilation skipped Running 1 test for test/OffchainAggregator.t.sol:OffchainAggregatorTest [PASS] test_transmit() (gas: 60497) Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.96ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests) ✨ Done in 0.765.

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.

Waiting for receipts. [O0:00:18]【######################################################】 1/1 receipts CO.0s) #####7796 ✅ [Success]Hash: Oxfb2f2dc6a35286c4595ce6e2bb64c93425b14c310a53f8224df0520666329fd ✅ Contract Address: OxBc788db88C3344a24706754c1203a267790D626 Block: 4049 Paid: 0.002392252 ETH (598063 gas * 4 gwei) Transactions saved to: /Users/tanssi/workspace/phalaMirrored/mirrored-price-feed/broadcast/OffchainAggregator.s.sol/7796/run-latest.json Sensitive values saved to: /Users/tanssi/workspace/phalaMirrored/mirrored-price-feed/cache/OffchainAggregator.s.sol/7796/run-latest.json ========================== ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. Total Paid: 0.002392252 ETH (598063 gas * avg 4 gwei) Transactions saved to: /Users/tanssi/workspace/phalaMirrored/mirrored-price-feed/broadcast/OffchainAggregator.s.sol/7796/run-latest.json Sensitive values saved to: /Users/kevin/workspace/phalaMirrored/mirrored-price-feed/cache/OffchainAggregator.s.sol/7796/run-latest.json ✨ Done in 30.765s.

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:

  1. Head to the Deploy and Run Transactions tab
  2. 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
  3. Select the AggregatorV3Interface contract from the CONTRACT dropdown
  4. 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

Access aggregator contract

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.

Get output of deployed aggregator contract

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 
npx tsx feeder.ts BTC-USD updated, transmit tx hash: Oxf1797cfc5bd71e2d848b099cae197ff30dafb5f6947481a5ef6c69271e059a96

Upon returning to Remix, click latestRoundData once more, and after waiting a moment, you should see an accurate value returned.

Check price data

For more information about using Phala to access off-chain data, be sure to check out the Phala docs site.

The information presented herein has been provided by third parties and is made available solely for general information purposes. Tanssi does not endorse any project listed and described on the Tanssi Doc Website (https://docs.tanssi.network/). Tanssi Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Tanssi Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Tanssi Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Tanssi Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Tanssi Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: March 29, 2024
| Created: February 15, 2024