A ROFL-powered price oracle that aggregates cryptocurrency prices from multiple off-chain sources and submits the median price to Sapphire smart contracts.
┌──────────────────────────────────────────────────────────────┐
│ Off-Chain (ROFL) │
├──────────────────────────────────────────────────────────────┤
│ Coinbase ──┐ │
│ Kraken ───┤ │
│ Bitstamp ──┼──► PriceAggregator ──► median ──► Observation │
│ CoinGecko ─┤ (outlier detection) Buffer │
│ CMC ───────┘ │ │
│ ▼ │
│ Every submit_period: Submit │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ On-Chain (Sapphire) │
├──────────────────────────────────────────────────────────────┤
│ PriceFeedDirectory │
│ └── feeds[keccak256("appId/aggregated/btc/usd")] │
│ └── SimpleAggregator (btc/usd) │
│ └── feeds[keccak256("appId/aggregated/eth/usd")] │
│ └── SimpleAggregator (eth/usd) │
└──────────────────────────────────────────────────────────────┘
- Multi-source aggregation: Queries multiple APIs (Coinbase, Kraken, CoinGecko, etc.)
- Median with outlier detection: Filters sources deviating >5% from median
- Drift limiting: Rejects sudden large price changes (configurable)
- Exponential backoff: Failed sources are temporarily excluded
- ROSE/USD support: Native support via Coinbase, CoinGecko, CoinMarketCap
- Python 3.11+
- Foundry (for contract deployment)
- Access to Oasis Sapphire network
# Clone and enter project root
cd rofl-price-oracle
# Install all dependencies (runtime + dev) via uv
make installThis runs uv sync --all-extras under the hood (see oracle/Makefile).
The oracle is always started via Docker Compose and configured through environment variables.
Use compose.local.yaml for local development against sapphire-localnet:
# 1. Start sapphire-localnet (if not already running)
docker run -it --rm -p8544-8548:8544-8548 --platform linux/x86_64 ghcr.io/oasisprotocol/sapphire-localnet
# 2. Deploy mock contracts
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
cd contracts && forge script script/DeployMocks.s.sol --rpc-url sapphire-localnet --broadcast && cd ..
# 3. Configure environment
cp .env.example .env
# Edit .env with your configuration (PAIRS, SOURCES, PRICE_FEED_ADDRESS, etc.)
# 4. Run with local compose file
docker compose -f compose.local.yaml up --buildThe compose.local.yaml uses RoflUtilityLocalnet with a hardcoded test key—no ROFL appd socket required.
Use compose.yaml for ROFL TEE deployment. The compose file is referenced within the ROFL app manifest.
# 1. Configure environment
cp .env.example .env
# Edit .env:
# - NETWORK=sapphire-testnet or NETWORK=sapphire
# - PRICE_FEED_ADDRESS=<your deployed PriceFeedDirectory>
# - API keys for paid sources (if any)
# 2. Build, update, and deploy the ROFL app
oasis rofl build
oasis rofl update
oasis rofl deployThe compose.yaml mounts /run/rofl-appd.sock for TEE-authenticated transactions via the ROFL runtime.
All configuration is done via environment variables in your .env file:
| Variable | Default | Description |
|---|---|---|
PAIRS |
btc/usd |
Comma-separated trading pairs |
SOURCES |
coinbase,kraken,bitstamp,coingecko |
Price sources to query |
MIN_SOURCES |
2 |
Minimum valid sources required |
MAX_DEVIATION_PERCENT |
5.0 |
Outlier threshold (%) |
DRIFT_LIMIT_PERCENT |
10.0 |
Max price change per round (0 to disable) |
FETCH_PERIOD |
60 |
Seconds between price fetches |
SUBMIT_PERIOD |
300 |
Seconds between on-chain submissions |
NETWORK |
sapphire-localnet |
Target network |
PRICE_FEED_ADDRESS |
— | PriceFeedDirectory contract address |
See .env.example for full documentation including API key configuration.
| Source | API Key | USD Pairs | ROSE Support |
|---|---|---|---|
coinbase |
No | Native | ✅ Yes |
kraken |
No | Native | ❌ No |
bitstamp |
No | Native | ❌ No |
coingecko |
Optional | All | ✅ Yes |
coinmarketcap |
Required | All | ✅ Yes |
coinapi |
Required | All | ✅ Yes |
eodhd |
Required | USD only | ✅ Yes |
binance |
No | USDT→USD | ✅ Yes |
Feel free to use the PriceFeedDirectory singleton on Sapphire to
discover public price feeds and register your own feeds:
| Contract | Sapphire Mainnet | Sapphire Testnet |
|---|---|---|
PriceFeedDirectory |
0x1e1A7E15dd6eEeD48e00530d31fCf408F40E0A12 |
0xB3E8721A5E9bb84Cfa99b50131Ac47341B4a9EfF |
Oasis maintains the following AggregatorV3Interface trading pair price
feeds on Sapphire which you can use to build your DeFi dapp:
| Trading pair | Sapphire Mainnet | Sapphire Testnet |
|---|---|---|
binance.us/rose/usdt |
0x9063375dc7A8f125d31DA43b8a02B1e065bAa081 |
0x47EFD60558012A64649c709b350f20C7a5f5e2Aa |
binance.com/rose/usdc |
0xB14E3b717f9ddff678403ed7fF26614D23FBd99a |
0x666938f7FBC353227F98DA43C050C8252eBfC0f7 |
binance.us/usdt/usd |
0xc8E6dEed5876Ee577252ecB70DA95286a5107D78 |
TBA |
binance.com/usdc/usd |
0xAC850546C3FFCA66A7D258eF14DF71135B55B44F |
TBA |
binance.us/eth/usdt |
TBA | 0xcE4c39fAe52C0a723c275Ab0949F84d783aF7A38 |
binance.com/eth/usdc |
TBA | 0x01a6F876411B35102B7f30D801162dDE9b7593e6 |
bitstamp.net/usdc/usd |
TBA | 0x9F9929a1A6510Ff289C4e0B1357b6dfF9fC1BB20 |
bitstamp.net/usdt/usd |
TBA | 0xd29802275E41449f675A2650629fBB268D2Ab52d |
bitstamp.net/usdc/usdt |
TBA | 0x1BeC39e4ca3B1Da500261333005578d8CA6A21b4 |
Solidity contracts are in the contracts folder.
cd contracts
soldeer installforge create \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://localhost:8545 \
--broadcast \
PriceFeedDirectoryThe oracle will automatically deploy SimpleAggregator contracts for each
trading pair and register them in the PriceFeedDirectory.
# Testnet
forge create ... --rpc-url https://testnet.sapphire.oasis.io
# Mainnet
forge create ... --rpc-url https://sapphire.oasis.ioMock contracts are provided for local development without TEE verification:
MockPriceFeedDirectory- No ROFL app ID verificationMockSimpleAggregator- No TEE check onsubmitObservation()
See Local Testing in the Running section above for full setup instructions.
-
Build Sapphire precompiles:
pushd contracts/dependencies/@oasisprotocol-sapphire-foundry-0.1.2/precompiles cargo build --release popd
-
Run tests:
cd contracts forge test
Makefile # Root Make targets (install, test, lint, run)
pyproject.toml # Project metadata, dependencies, Ruff config
oracle/
├── main.py # CLI entry point
├── src/
│ ├── AggregatedPair.py # Trading pair representation
│ ├── PriceAggregator.py # Median aggregation with outlier detection
│ ├── SourceManager.py # Per-source failure tracking & backoff
│ ├── PriceOracle.py # Main orchestrator
│ ├── ContractUtility.py # Contract ABI loading
│ ├── RoflUtility*.py # ROFL appd integration
│ └── fetchers/ # Price source implementations
│ ├── base.py # Abstract fetcher interface
│ ├── coinbase.py
│ ├── kraken.py
│ ├── bitstamp.py
│ ├── coingecko.py
│ ├── coinmarketcap.py
│ ├── coinapi.py
│ ├── eodhd.py
│ ├── bitquery.py
│ └── binance.py
└── tests/ # Unit tests
contracts/
├── src/
│ ├── PriceFeedDirectory.sol # Feed registry
│ ├── SimpleAggregator.sol # Per-pair aggregator
│ ├── RoflAggregatorV3Interface.sol
│ └── mocks/ # Mock contracts for local testing
│ ├── MockPriceFeedDirectory.sol
│ └── MockSimpleAggregator.sol
├── script/
│ └── DeployMocks.s.sol # Foundry deployment script
└── test/
From the project root:
# Install dependencies (runtime + dev)
make install
# Run unit tests
make test # equivalent to: python -m pytest oracle/tests
# Lint with Ruff
make lint # equivalent to: python -m ruff check oracle/src oracle/tests
# Run both lint and tests
make check- Fetch: Query all active sources concurrently
- Filter: Remove None/zero/negative prices
- Initial Median: Calculate median of valid prices
- Outlier Detection: Exclude sources >5% from initial median
- Final Median: Recalculate from filtered set
- Drift Check: Reject if >10% change from previous round
- Accumulate: Store observation with timestamp
- Submit: Every
submit_period, take median of observations and submit on-chain
- CoinGecko free: 30 calls/min → Safe with
FETCH_PERIOD=60 - CoinMarketCap free: 333 calls/day → Use
SUBMIT_PERIOD≥300(5 min) - Coinbase/Kraken/Bitstamp: High limits, no key required
MIT