A Safer Harbor For Leverage, Uncharted Waters For Yield.
Price oracle aggregators for Harbor Protocol.
The v3 aggregators use an immutable architecture: each aggregator is a concrete contract with configuration baked in at construction time. Network-specific "wiring" files (src/Aggregator_*_mainnet.sol) extend formula contracts (src/aggregators/Aggregator_*.sol) and pass chain-specific feed addresses and heartbeats to the constructor.
Architecture highlights:
- Immutable configuration: Feed addresses, heartbeats, and rate sources are constructor parameters (no
initialize()or storage slots) - Heartbeat validation:
ChainlinkFeedLibvalidates feed freshness with a 42-second tolerance to account for block timing variance - UUPS Upgradeable: Proxy pattern via BaoFactory with fixed owner
To add a new aggregator, see doc/v3-aggregator-authoring-guide.md.
| Pair | Oracle | Rate Source | Feeds |
|---|---|---|---|
| fxUSD/ETH | fxSAVE | ETH/USD (inverted) | |
| fxUSD/BTC | fxSAVE | BTC/USD | |
| fxUSD/EUR | fxSAVE | EUR/USD (inverted) | |
| fxUSD/GOLD | fxSAVE | XAU/USD | |
| fxUSD/MCAP | fxSAVE | MCAP/USD | |
| fxUSD/SILVER | fxSAVE | XAG/USD | |
| stETH/BTC | wstETH | ETH/USD, BTC/USD | |
| stETH/EUR | wstETH | ETH/USD, EUR/USD (inverted) | |
| stETH/GOLD | wstETH | ETH/USD, XAU/USD | |
| stETH/MCAP | wstETH | ETH/USD, MCAP/USD | |
| stETH/SILVER | wstETH | ETH/USD, XAG/USD |
GOLD / SILVER vs XAU / XAG: We use GOLD and SILVER for external naming (oracle names, mainnet wiring in src/mainnet, deploy scripts). The formula contracts in src/aggregators/mainnet and Chainlink feeds stay as XAU (gold) and XAG (silver); mainnet contracts extend those and expose the quote as GOLD or SILVER. No duplicate XAU/XAG mainnet wiring—only GOLD and SILVER are deployed.
Single-feed leveraged token oracles (e.g. hsfxUSD-BTC/USD) use the same formula contract for both USD- and BTC-denominated output. Only the constructor’s last argument (invert) changes:
invert |
Output |
|---|---|
false |
Price in USD — output = rate × BTC/USD |
true |
Price in BTC (leverage terms) — output = rate × (1e18 / BTC/USD) |
No other code changes are required: same formula contract, same mainnet contract type; wire a separate mainnet deployment with invert = true if you want a feed in BTC instead of USD.
Current mainnet deployments expose the USD price (invert = false). Integrators should use these feeds when consuming the leveraged token oracles (hsfxUSD-, hsstETH-).
| Oracle | Rate Source | Feeds |
|---|---|---|
| USDE/AAPL | sUSDE/USDE (Chainlink) | USDE/USD, AAPL/USD |
| USDE/AMZN | sUSDE/USDE (Chainlink) | USDE/USD, AMZN/USD |
| USDE/GOOGL | sUSDE/USDE (Chainlink) | USDE/USD, GOOGL/USD |
| USDE/META | sUSDE/USDE (Chainlink) | USDE/USD, META/USD |
| USDE/MSFT | sUSDE/USDE (Chainlink) | USDE/USD, MSFT/USD |
| USDE/NVDA | sUSDE/USDE (Chainlink) | USDE/USD, NVDA/USD |
| USDE/SPY | sUSDE/USDE (Chainlink) | USDE/USD, SPY/USD |
| USDE/TSLA | sUSDE/USDE (Chainlink) | USDE/USD, TSLA/USD |
| USDE/MAG7 | sUSDE/USDE (Chainlink) | USDE/USD, (AAPL+MSFT+TSLA+GOOGL+META+AMZN+NVDA)/7 |
| USDE/MAG7.i26 | sUSDE/USDE (Chainlink) | USDE/USD, (AAPL+MSFT+TSLA+GOOGL+META+AMZN+NVDA)/index_price |
| stETH/AAPL | wstETH/stETH (Chainlink) | stETH/USD, AAPL/USD |
| stETH/AMZN | wstETH/stETH (Chainlink) | stETH/USD, AMZN/USD |
| stETH/GOOGL | wstETH/stETH (Chainlink) | stETH/USD, GOOGL/USD |
| stETH/META | wstETH/stETH (Chainlink) | stETH/USD, META/USD |
| stETH/MSFT | wstETH/stETH (Chainlink) | stETH/USD, MSFT/USD |
| stETH/NVDA | wstETH/stETH (Chainlink) | stETH/USD, NVDA/USD |
| stETH/SPY | wstETH/stETH (Chainlink) | stETH/USD, SPY/USD |
| stETH/TSLA | wstETH/stETH (Chainlink) | stETH/USD, TSLA/USD |
| stETH/MAG7 | wstETH/stETH (Chainlink) | stETH/USD, (AAPL+MSFT+TSLA+GOOGL+META+AMZN+NVDA)/7 |
| stETH/MAG7.i26 | wstETH/stETH (Chainlink) | stETH/USD, (AAPL+MSFT+TSLA+GOOGL+META+AMZN+NVDA)/index_price |
| Oracle | Rate Source | Feeds |
|---|---|---|
| stETH/BOM5 | wstETH/stETH (Chainlink) | stETH/USD, normalized average of (DOGE+SHIB+PEPE+TRUMP+WIF)/5 |
# Install Foundry if not already installed
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Install dependencies
forge install
yarn
uv sync
Run all tests:
yarn test
Run tests for specific chains:
# Mainnet tests (unit tests only, no fork required)
forge test --match-path "test/oracles/*.t.sol"
# Arbitrum tests (unit tests, no RPC required)
forge test --match-path "test/arbitrum/Aggregator_*.t.sol"
# Arbitrum fork tests (requires ARBITRUM_RPC_URL)
forge test --match-path "test/arbitrum/*Fork.t.sol" --fork-url $arbitrum -vvv
# Base tests (unit tests, no RPC required)
forge test --match-path "test/base/Aggregator_*.t.sol"
# Base fork tests (requires BASE_RPC_URL)
forge test --match-path "test/base/*Fork.t.sol" --fork-url $base -vvvNote: Make sure to set ARBITRUM_RPC_URL, BASE_RPC_URL, and MAINNET_RPC_URL in your .env file for fork tests. See test/arbitrum/README.md and test/base/README.md for detailed testing information.
For deploying and verifying aggregator implementations and proxies, see script/README.md.
The repo includes a small Python tool, offchain_feeds, that downloads and stores daily time series (UTC day boundaries) and computes basic stats.
yarn uvIf you use CoinGecko sources, you may need a (free) Demo API key:
export COINGECKO_API_KEY=your_coingecko_demo_key- Extract to
<ROOT>/daily/<MARKET>.parquet(default<ROOT>isdata/offchain):yarn offchain:extract --market <MARKET> --source <SOURCE> --start <YYYY-MM-DD> --end <YYYY-MM-DD> [--root <ROOT>]
- Compute extremes from the stored parquet:
yarn offchain:stats --market <MARKET> --measure close_return --top 20 [--root <ROOT>]yarn offchain:stats --market <MARKET> --measure intraday_range --top 20 [--root <ROOT>]
- Show embedded parquet metadata:
yarn offchain:meta --market <MARKET> [--root <ROOT>] [--pretty]
Notes:
intraday_rangerequireshigh/lowand will fail for close-only datasets.- Sources are market-specific in the current implementation (a source errors if it doesn’t support a market), so run separate extract commands per market.
There is an additional stats mode that treats the close-to-close returns as a random variable, fits a volatility model (GARCH), then fits a parametric curve to the standardized returns.
Example:
yarn offchain:stats --market ETH-BTC --measure return_fit --top 10 --return-kind log --dist auto --buckets --plot results/eth-btc-return-fit.pngWhat it prints:
- A JSON block describing the fitted model parameters.
- A
--toptable of the most “surprising” days under the fitted model. - (Optional) a “fitted histogram” in 1% buckets, plus empirical frequencies.
Return kind:
-
simple return:
$r = C_t / C_{t-1} - 1$ (this is the existingclose_returnmeasure) -
log return:
$\ell = \ln(C_t / C_{t-1})$ (often more statistically convenient)
Probability / score columns (layman descriptions):
-
Two-sided tail probability (
p_two_sided): “how extreme is this move (up or down) compared to the model?”- Smaller means more extreme.
- It’s a p-value style score:
$p = 2\min(F(z), 1-F(z))$ where$z$ is the standardized return.
-
One-sided tail probability (
p_one_sided): “how extreme is this move in the direction it occurred?”- Smaller means a rarer move on that side.
-
PDF density (
pdf_density): “how concentrated the fitted curve is at exactly this return value.”- This is a density, not a probability; higher usually means more ‘typical’.
-
1% bucket probability (
p_bucket): “what is the model probability that the return lands in this 1% bin?”- This is a true probability mass for the interval (e.g. [1%, 2%)).
- Useful when you want histogram-style probabilities.
Some providers only list one direction of a market (e.g. Coinbase has ETH-BTC but not BTC-ETH).
extract: if the requested market 404s but the inverse exists, the tool will fetch the inverse and mathematically invert OHLC (e.g.close' = 1/close, andhigh/lowswap accordingly).stats/export/meta: if<ROOT>/daily/<MARKET>.parquetis missing but the inverse parquet exists, the tool will read the inverse and invert it on the fly.
These 9 “feeds” correspond to the v3 mainnet wrapper inventory in V3_MAINNET_ORACLES_WORKPLAN.md. The extractor stores the underlying input markets used by those feeds (not the derived on-chain wrapper output series).
Use START=2017-01-01 (or later) and END=2025-12-24 (or today).
- fxUSD/ETH (extract
ETH/USD)
yarn offchain:extract --market ETH-USD --source coinbase --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market ETH-USD --measure close_return --top 20
yarn offchain:stats --market ETH-USD --measure intraday_range --top 20- fxUSD/BTC (extract
BTC/USD)
yarn offchain:extract --market BTC-USD --source coinbase --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market BTC-USD --measure close_return --top 20
yarn offchain:stats --market BTC-USD --measure intraday_range --top 20- fxUSD/EUR (extract
EUR/USD)
yarn offchain:extract --market EUR-USD --source fred --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market EUR-USD --measure close_return --top 20- fxUSD/XAU (extract
XAU/USD)
yarn offchain:extract --market XAU-USD --source fred --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market XAU-USD --measure close_return --top 20- fxUSD/MCAP (extract
MCAP/USD)
yarn offchain:extract --market MCAP-USD --source coingecko --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market MCAP-USD --measure close_return --top 20- stETH/BTC (extract
stETH/USDandBTC/USD)
yarn offchain:extract --market STETH-USD --source coingecko --start 2017-01-01 --end 2025-12-24
yarn offchain:extract --market BTC-USD --source coinbase --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market STETH-USD --measure close_return --top 20
yarn offchain:stats --market BTC-USD --measure close_return --top 20- stETH/EUR (extract
stETH/USDandEUR/USD)
yarn offchain:extract --market STETH-USD --source coingecko --start 2017-01-01 --end 2025-12-24
yarn offchain:extract --market EUR-USD --source fred --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market STETH-USD --measure close_return --top 20
yarn offchain:stats --market EUR-USD --measure close_return --top 20- stETH/XAU (extract
stETH/USDandXAU/USD)
yarn offchain:extract --market STETH-USD --source coingecko --start 2017-01-01 --end 2025-12-24
yarn offchain:extract --market XAU-USD --source fred --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market STETH-USD --measure close_return --top 20
yarn offchain:stats --market XAU-USD --measure close_return --top 20- stETH/MCAP (extract
stETH/USDandMCAP/USD)
yarn offchain:extract --market STETH-USD --source coingecko --start 2017-01-01 --end 2025-12-24
yarn offchain:extract --market MCAP-USD --source coingecko --start 2017-01-01 --end 2025-12-24
yarn offchain:stats --market STETH-USD --measure close_return --top 20
yarn offchain:stats --market MCAP-USD --measure close_return --top 20To deploy sUSDe and other v4 oracles (wstETH/USD, wBTC/USD, tBTC/USD, PAXG/USD, and sUSDe: BTC, ETH, EUR, MCAP, GOLD, SILVER):
# Requires: MAINNET_RPC_URL, PRIVATE_KEY (and ETHERSCAN_API_KEY for verify)
./script/deploy-mainnet-v4-oracles.shState: deployments/mainnet/v4-oracles.json. Verify: ./script/verify-mainnet-v4-oracles.sh
To deploy leveraged token USD oracles (hsfxUSD-, hsstETH-):
./script/deploy-mainnet-leverage-v4-oracles.shState: deployments/mainnet/leverage-v4-oracles.json. Verify: ./script/verify-mainnet-leverage-v4-oracles.sh
Deploy and verify v4 and leverage separately; they are independent.
-
Configure foundry.toml RPC endpoints (for testing): Add your RPC URLs to
foundry.tomlunder[rpc_endpoints]:[rpc_endpoints] mainnet = "${MAINNET_RPC_URL}" arbitrum = "${ARBITRUM_RPC_URL}" base = "${BASE_RPC_URL}" local = "http://127.0.0.1:8545"
Or set environment variables in
.env:MAINNET_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY" ARBITRUM_RPC_URL="https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY" BASE_RPC_URL="https://base-mainnet.g.alchemy.com/v2/YOUR_KEY"
-
Set up a Foundry keystore account (for non-local deploys):
cast wallet import deployer --interactive
-
Optional: Use Anvil fork for testing:
anvil -f mainnet --auto-impersonate
Deploy individual oracles using script/harbor-aggregators-v3:
# Deploy fxUSD/ETH to mainnet
./script/harbor-aggregators-v3 --network mainnet --base fxUSD --quote ETH --account deployer --deploy
# Deploy with local anvil fork (for testing)
./script/harbor-aggregators-v3 --network mainnet --local --base fxUSD --quote ETH --deploy
# Check deployment status
./script/harbor-aggregators-v3 --network mainnet --base fxUSD --quote ETH --check
# Verify on Etherscan
./script/harbor-aggregators-v3 --network mainnet --base fxUSD --quote ETH --etherscan-api-key YOUR_KEY --verifyModes:
--deploy: Deploy implementation + proxy via BaoFactory--deploy-impl: Deploy new implementation only (for upgrades; outputs Safe tx)--check: Verify deployment exists and returns valid data--verify: Verify both implementation and proxy on Etherscan--validate-args: Dry-run validation
Deployments are recorded in deployment-state-v3-<network>.json.
Deploy all Arbitrum v3 oracle contracts (direct deployments, no proxies):
./script/deploy-arbitrum-v3-oracles.shThis will deploy 20 v3 oracle contracts (immutable contracts with hardcoded wiring):
USDE oracles (10):
- USDE/AAPL, USDE/AMZN, USDE/GOOGL, USDE/META, USDE/MSFT, USDE/NVDA, USDE/SPY, USDE/TSLA, USDE/MAG7, USDE/MAG7.i26
stETH oracles (10):
- stETH/AAPL, stETH/AMZN, stETH/GOOGL, stETH/META, stETH/MSFT, stETH/NVDA, stETH/SPY, stETH/TSLA, stETH/MAG7, stETH/MAG7.i26
Requirements:
Set these environment variables (or add them to .env):
export ARBITRUM_RPC_URL="https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY"
export PRIVATE_KEY="your_private_key"
export ETHERSCAN_API_KEY="your_etherscan_api_key" # Optional, for verificationVerification:
After deployment, verify all contracts on Arbiscan:
./script/verify-arbitrum-v3-oracles.shOr run verification during deployment (automatic if ETHERSCAN_API_KEY is set).
Deployment State:
Deployments are recorded in deployments/arbitrum/v3-oracles.json. The script will skip contracts that are already deployed unless FORCE_REDEPLOY=true is set.
Note: These are direct deployments (immutable contracts), not proxy deployments. Each contract is deployed independently with its configuration baked into the bytecode.
Deploy Base v3 oracle contracts (direct deployments, no proxies):
# Deployment script to be added
# ./script/deploy-base-v3-oracles.shBase oracles:
- stETH/BOM5 (Bag of Memes 5: DOGE, SHIB, PEPE, TRUMP, WIF with supply normalization)
Requirements:
Set these environment variables (or add them to .env):
export BASE_RPC_URL="https://base-mainnet.g.alchemy.com/v2/YOUR_KEY"
export PRIVATE_KEY="your_private_key"
export ETHERSCAN_API_KEY="your_etherscan_api_key" # Optional, for verificationNote: Base deployment scripts will follow the same pattern as Arbitrum deployments (direct immutable contracts with hardcoded wiring).
MIT