Trustless Price Oracle
This chapter will show you how to quickly create, build and test a minimal containerized ROFL-powered app that authenticates and communicates with a confidential smart contract on Oasis Sapphire.
Prerequisites
This guide requires:
- a working Docker (or Podman),
- Oasis CLI and
- at least 120 TEST tokens in your wallet.
Check out the Quickstart Prerequisites section for details.
Init App
First we init the basic directory structure for the app using the Oasis CLI:
oasis rofl init rofl-price-oracle
cd rofl-price-oracle
Create App
Now create an app on Testnet (requires deposit of 100 TEST):
oasis rofl create --network testnet
After successful creation, the CLI will also output the new identifier, for example:
Created ROFL application: rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
Oracle Contract
While we are using EVM-based smart contracts in this example, the on-chain part can be anything from a WASM-based smart contract to a dedicated runtime module.
We will use the following smart contract:
Oracle.sol
pragma solidity >=0.8.9 <=0.8.24;
import {Subcall} from "@oasisprotocol/sapphire-contracts/contracts/Subcall.sol";
contract Oracle {
// Maximum age of observations.
uint private constant MAX_OBSERVATION_AGE = 10;
// Configuration.
uint8 public threshold;
bytes21 public roflAppID;
// Observations.
struct Observation {
uint128 value;
uint block;
}
uint128[] private observations;
Observation private lastObservation;
constructor(bytes21 _roflAppID, uint8 _threshold) {
require(_threshold > 0, "Invalid threshold");
roflAppID = _roflAppID;
threshold = _threshold;
lastObservation.value = 0;
lastObservation.block = 0;
}
function submitObservation(uint128 _value) external {
// Ensure only the authorized ROFL app can submit.
Subcall.roflEnsureAuthorizedOrigin(roflAppID);
// NOTE: This is a naive oracle implementation for ROFL example purposes.
// A real oracle must do additional checks and better aggregation before
// accepting values.
// Add observation and check if we have enough for this round.
observations.push(_value);
if (observations.length < threshold) {
return;
}
// Simple averaging.
uint256 _agg = 0;
for (uint i = 0; i < observations.length; i++) {
_agg += uint256(observations[i]);
}
_agg = _agg / uint128(observations.length);
lastObservation.value = uint128(_agg);
lastObservation.block = block.number;
delete observations;
}
function getLastObservation() external view returns (uint128 _value, uint _block) {
// Last observation must be fresh enough, otherwise we don't disclose it.
require(
lastObservation.block + MAX_OBSERVATION_AGE > block.number,
"No observation available"
);
_value = lastObservation.value;
_block = lastObservation.block;
}
}
This contract collects observations from authenticated application on ROFL,
performs trivial aggregation and stores the final aggregated result. Read the
Sapphire quickstart chapter to learn how to build and deploy smart contracts
on Sapphire, but to get you up and running for this part, simply copy the oracle
folder from the example project, install dependencies and compile the smart
contract by executing:
cd oracle
npm install
npx hardhat compile
Then configure the PRIVATE_KEY
of the deployment account and the app
ID you received in the previous step.
Then deploy the contract by running:
PRIVATE_KEY="0xYOUR_PRIVATE_KEY" \
npx hardhat deploy YOUR_APP_ID --network sapphire-testnet
After successful deployment you will see a message like:
Oracle for ROFL app rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf deployed to 0x1234845aaB7b6CD88c7fAd9E9E1cf07638805b20
Remember the address where the oracle contract was deployed to as you will need it in the next step.
Oracle Worker in Container
Inside docker
folder add a simple shell script which downloads price quotes
from a centralized exchange (Binance in our case) and sends it to
our contract using the appd REST API.
app.sh
#!/bin/sh
while true; do
# Fetch a recent price from Binance.
price=$(curl -s "https://www.binance.com/api/v3/ticker/price?symbol=${TICKER}" | jq '(.price | tonumber) * 1000000 | trunc')
if [ -z "$price" ]; then
sleep 15
continue
fi
# Format calldata to call submitObservation(uint128) method with the price.
price_u128=$(printf '%064x' ${price})
method="dae1ee1f" # Keccak4("submitObservation(uint128)")
data="${method}${price_u128}"
# Submit it to the Sapphire contract.
curl -s \
--json '{"tx": {"kind": "eth", "data": {"gas_limit": 200000, "to": "'${CONTRACT_ADDRESS}'", "value": 0, "data": "'${data}'"}}}' \
--unix-socket /run/rofl-appd.sock \
http://localhost/rofl/v1/tx/sign-submit >/dev/null
# Sleep for a while.
sleep 60
done
Similarly, inside the docker
folder add a Dockerfile
that copies over the
shell script to a container:
Dockerfile
FROM docker.io/alpine:3.21.2
# Add some dependencies.
RUN apk add --no-cache curl jq
# The entire application is defined as a shell script.
ADD app.sh /app.sh
ENTRYPOINT ["/app.sh"]
Compose
Add a compose.yaml
to the root of your project:
compose.yaml
services:
oracle:
build: ./docker
image: docker.io/YOUR_USERNAME/rofl-price-oracle:latest
platform: linux/amd64
environment:
# Address of the oracle contract deployed on Sapphire Testnet.
- CONTRACT_ADDRESS=YOUR_CONTRACT_ADDRESS
# Ticker.
- TICKER=ROSEUSDT
volumes:
- /run/rofl-appd.sock:/run/rofl-appd.sock
Now build and push the image to a registry
docker compose build
docker compose push
For extra security, you can pin the image digest inside compose.yaml
.
Build
To build an app and update the enclave identity in the app manifest, run:
- Native Linux
- Docker (Windows, Mac, Linux)
oasis rofl build
docker run --platform linux/amd64 --volume .:/src -it ghcr.io/oasisprotocol/rofl-dev:main oasis rofl build
This will generate the ROFL bundle which can be used for later deployment and output something like:
ROFL app built and bundle written to 'rofl-price-oracle.default.orc'.
Update On-chain App Config
The on-chain app config needs to be updated in order for the changes to take effect:
oasis rofl update
Deploy to ROFL provider
Deploy the price oracle to one of the ROFL providers:
oasis rofl deploy
By default, the provider maintained by the Oasis foundation will be picked.
Check That the Oracle Contract is Getting Updated
To check whether the oracle is actually working, you can use the prepared
oracle-query
task in the Hardhat project. Simply run:
cd oracle
npx hardhat oracle-query 0x1234845aaB7b6CD88c7fAd9E9E1cf07638805b20 --network sapphire-testnet
And you should get an output like the following:
Using oracle contract deployed at 0x1234845aaB7b6CD88c7fAd9E9E1cf07638805b20
ROFL app: rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
Threshold: 1
Last observation: 63990
Last update at: 656
That's it! Your first ROFL oracle that authenticates with an Oasis Sapphire smart contract is running! 🎉
Price Oracle Demo
You can fetch a complete example shown in this chapter from https://github.com/oasisprotocol/demo-rofl.