sfi-oracle-sdk
v0.2.6
Published
ABIs and deployed addresses for the SFI modular Oracle framework
Maintainers
Readme
Oracle Framework
A small, modular Solidity framework for pricing ERC-20 tokens, ERC-4626 vault shares, and registered Balancer V2 pool tokens in a configurable reference asset.
This repository was extracted from DynaVaults so the Oracle layer can be compiled, tested, reviewed, and deployed independently of the vault, strategy, router, liquidity, and zapper frameworks.
The repository also publishes sfi-oracle-sdk, a zero-runtime-dependency npm package containing the generated contract ABIs and verified deployment addresses.
Highlights
- Chainlink feed-registry pricing with WETH, WBTC, USD, ETH, and BTC routing.
- Uniswap V3 time-weighted pricing with configurable fee tiers, observation period, and minimum liquidity.
- Preferred-source aggregation with ordered fallbacks and per-oracle pausing.
- ERC-4626 and Balancer V2 adapters that value wrapped positions through the aggregate oracle.
- Clonable reference-asset proxy oracles, an oracle registry, and an oracle factory.
- A balance quoter for converting wallet holdings into a selected quote asset.
- Typed ABI and deployed-address exports for TypeScript, ESM, and CommonJS consumers.
- Fully deterministic local tests; Ethereum, Arbitrum One, BNB Chain, Polygon PoS, OP Mainnet, and Base fork suites remain available separately.
SDK
Install the package:
npm install sfi-oracle-sdkImport an ABI and the preconfigured registry address for the connected chain:
import { getOracleRegistryAddress, oracleRegistryAbi, requireOracleRegistryAddress } from "sfi-oracle-sdk";
const chainId = 8453;
// Returns undefined when the framework is not deployed on the chain.
const maybeRegistry = getOracleRegistryAddress(chainId);
// Throws a descriptive error when the chain is unsupported.
const registry = requireOracleRegistryAddress(chainId);The ABI is a readonly literal compatible with clients such as viem:
const oracle = await publicClient.readContract({
address: requireOracleRegistryAddress(publicClient.chain.id),
abi: oracleRegistryAbi,
functionName: "oracles",
args: [referenceAsset],
});Tree-shakeable subpath exports and CommonJS are also supported:
import { referenceAssetOracleAbi } from "sfi-oracle-sdk/abis";
import {
arbitrumOracleDeployment,
baseOracleDeployment,
ethereumOracleDeployment,
optimismOracleDeployment,
oracleRegistryAddresses,
} from "sfi-oracle-sdk/chains";const { oracleRegistryAbi, requireOracleRegistryAddress } = require("sfi-oracle-sdk");SDK exports
- Named ABI constants use lower camel case followed by
Abi, such asoracleRegistryAbi,referenceAssetOracleAbi, anduniswapV3OracleAbi. oracleAbisprovides the same ABIs by Solidity contract name, for exampleoracleAbis.OracleRegistry.oracleDeploymentsandORACLE_DEPLOYMENTSmap chain IDs to complete typed manifests.oracleRegistryAddressesandORACLE_REGISTRY_ADDRESSESmap chain IDs directly to registry addresses.getOracleDeploymentandgetOracleRegistryAddressreturnundefinedfor unsupported chains.requireOracleDeploymentandrequireOracleRegistryAddressthrow for unsupported chains.chainIdsandisSupportedChainmake supported-chain checks explicit.
The package includes ABIs for BalanceQuoter, BalancerV2WeightedPoolOracle, ChainlinkOracle, ERC4626Oracle, IReferenceAssetOracle, MinimalChainlinkFeedRegistry, OracleFactory, OracleRegistry, ProxyReferenceAssetOracle, ReferenceAssetOracle, UniswapV3Oracle, and WrappedNativePriceFeed.
Published deployments
| Chain | Chain ID | Oracle registry | Deployment block |
| ------------ | -------: | -------------------------------------------- | ---------------: |
| Ethereum | 1 | 0xF569EECfc55811FbCa5f2AAAd20715b0AC35265B | 25,677,917 |
| OP Mainnet | 10 | 0x8a7f47da04911307C0AbA48E48BC4E3165bA976f | 155,513,751 |
| BNB Chain | 56 | 0x2208a59C1ACb6e42B6d722c7FE1ED3FdfE49E7f9 | 114,729,142 |
| Polygon PoS | 137 | 0x6aB825bB2B5F03e188Ac681F1346b7871d5FfCCA | 91,945,887 |
| Base | 8453 | 0x184B2217FC07ECFA77C7a6DF476b0814250bE877 | 48,808,465 |
| Arbitrum One | 42161 | 0xeEc9F7f0CCea10BB0d08E6355c2456e85862a485 | 494,036,797 |
Each manifest also exports its deployed factory, source and adapter oracles, proxy implementation, minimal Chainlink feed registry, balance quoter, and reference asset. The Ethereum manifest additionally includes the wrapped-native price feed and WETH proxy oracle; the OP Mainnet and Arbitrum One manifests include their wrapped-native price feeds plus WETH, WBTC, and USDT proxy oracles; the BNB Chain manifest includes its wrapped-native price feed plus WBNB, BTCB, and USDT proxy oracles; and the Polygon PoS manifest includes its wrapped-native price feed plus USDT, WPOL, WETH, and WBTC proxy oracles. Addresses are included only after they have been recorded by the deployment configuration and verified to contain contract bytecode.
Architecture
flowchart LR
SOURCE[Configured Chainlink registry] -. deployment snapshot .-> MINIMAL[MinimalChainlinkFeedRegistry]
MINIMAL --> CL[ChainlinkOracle]
CL --> RAO[ReferenceAssetOracle]
UNI[UniswapV3Oracle] --> RAO
SPOT[SpotOracle - test only] -.-> RAO
RAO <--> E4626[ERC4626Oracle]
RAO <--> BAL[BalancerV2WeightedPoolOracle]
RAO --> PROXY[ProxyReferenceAssetOracle]
FACTORY[OracleFactory] --> PROXY
FACTORY --> REGISTRY[OracleRegistry]
RAO --> REGISTRY
REGISTRY --> QUOTER[BalanceQuoter]
RAO --> CONSUMERS[Vaults and other consumers]ReferenceAssetOracle first queries its preferred source. If that source is paused, reverts, or returns an observation timestamp of zero, it walks the configured fallback list. The ERC-4626 and Balancer adapters may themselves call the aggregate oracle to value their underlying assets.
Contract reference
| Contract | Purpose |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| ChainlinkOracle | Reads and normalizes Chainlink feed-registry prices. Routes wrapped-native, wrapped-BTC, USD, ETH, and BTC pairs where a direct feed is unavailable. |
| UniswapV3Oracle | Calculates time-weighted quotes from Uniswap V3 observations. Supports direct pools and WETH-routed pairs. |
| ReferenceAssetOracle | Aggregates one preferred oracle and an ordered list of fallback oracles. Supports pausing individual sources. |
| ERC4626Oracle | Converts registered vault shares to assets with convertToAssets, then values the underlying asset. |
| BalancerV2WeightedPoolOracle | Values registered Balancer pool tokens from their underlying balances and actual supply. |
| ProxyReferenceAssetOracle | A clonable view that expresses prices in another reference asset while delegating to a base oracle. |
| OracleFactory | Creates EIP-1167 proxy-oracle clones and registers them. |
| OracleRegistry | Maps a reference asset to its oracle. |
| BalanceQuoter | Returns wallet balances and their values through an oracle selected from the registry. |
| MinimalChainlinkFeedRegistry | Stores the feed snapshot used by ChainlinkOracle and exposes its required feeds, decimals, and latestRoundData API. |
| WrappedNativePriceFeed | Exposes the protocol-guaranteed 1:1 wrapped-native/native exchange rate through the aggregator interface used by the minimal registry. |
Price interface
All pricing implementations expose IReferenceAssetOracle:
interface IReferenceAssetOracle {
function referenceAsset() external view returns (address);
function tokenReferenceValue(address token, uint256 amount) external view returns (uint256 referenceValue, uint256 oldestObservation);
function getPrice(address base, address quote) external view returns (uint256 value, uint256 oldestObservation);
}tokenReferenceValue returns a value denominated in the reference asset's native decimals. getPrice(base, quote) returns the quote-token amount for one whole base token, using the quote token's decimals. oldestObservation is the oldest timestamp used to produce the result. Unsupported or stale routes generally return (0, 0); callers must treat that pair as an invalid price.
Requirements
- Node.js 18 or newer
- npm
- An EVM RPC endpoint only when running fork tests or deploying
Framework development
git clone [email protected]:machieke/oracle-framework.git
cd oracle-framework
npm install
npm run compileGenerated Hardhat artifacts and TypeChain bindings are written to artifacts/ and typechain/; both are ignored by Git.
Build the SDK
The ABI generator reads the Hardhat artifacts so published ABIs stay aligned with the Solidity sources:
npm run sdk:build
npm run sdk:test
npm run sdk:packsdk:test verifies ESM, CommonJS, subpath exports, address helpers, and TypeScript package consumption. npm publish runs the build and SDK tests again through the prepack lifecycle hook.
Test and quality commands
npm test # deterministic local suite
npm run test:deploy:mainnet-fork # deploy the complete stack on an Ethereum fork
npm run test:deploy:bnb-fork # deploy the complete stack on a BNB Chain fork
npm run test:deploy:arbitrum-fork # deploy the complete stack on an Arbitrum One fork
npm run test:deploy:polygon-fork # deploy the complete stack on a Polygon PoS fork
npm run test:deploy:optimism-fork # deploy the complete stack on an OP Mainnet fork
npm run coverage # Solidity coverage report
npm run typecheck # TypeScript validation
npm run lint # Solidity lintingThe local suite currently contains 31 tests and does not require an RPC endpoint. With contracts/test, contracts/utils, and contracts/interfaces excluded, the current included-source coverage is:
| Metric | Coverage | | ---------- | -------: | | Statements | 100% | | Branches | 91.98% | | Functions | 100% | | Lines | 100% |
Coverage output is written to coverage/ and coverage.json.
Fork tests
The copied DynaVaults integration suites exercise live Ethereum and Base contracts. Configure RPC endpoints before running them:
cp .env.example .env
# Set MAINNET_RPC_URL, ARBITRUM_RPC_URL, BNB_RPC_URL, POLYGON_RPC_URL, OPTIMISM_RPC_URL, and BASE_RPC_URL in .env as needed
npm run test:integrationRun both local and fork suites with npm run test:all. Fork tests depend on current remote chain state, RPC rate limits, archive-state availability, and historical DynaVaults deployments.
Deployment
The standalone deployer is deployment-scripts/deploy-oracle-framework.ts. It accepts a typed JSON configuration, deploys only the selected modules, configures fallbacks and adapters, and writes a deployment manifest. Every deployment and state-changing call uses a conservative RPC-based EIP-1559 fee strategy. Its CLI path also uses sfi-deployment-sdk for deployment phases, transaction tracking, and the final gas-cost report. The exported deployOracleFramework function accepts an optional fee provider as its fourth argument, which lets fork tests price transactions from the upstream network instead of Hardhat's synthetic fee history.
Create an environment file:
cp .env.example .envSet the deployer key and the RPC endpoint for the target network:
DEPLOYER_PRIVATE_KEY=0x... MAINNET_RPC_URL=https://... ARBITRUM_RPC_URL=https://... BNB_RPC_URL=https://... POLYGON_RPC_URL=https://... OPTIMISM_RPC_URL=https://... BASE_RPC_URL=https://... FORKED_CHAIN_ID=1Copy and edit the example configuration:
cp deployment-scripts/oracle-framework.config.example.json \ deployment-scripts/oracle-framework.config.jsonCompile and deploy:
npm run compile ORACLE_CONFIG=deployment-scripts/oracle-framework.config.json \ npm run deploy -- --network mainnetTo deploy to an Ethereum fork exposed at
LOCAL_RPC_URL, keepFORKED_CHAIN_ID=1and run:ORACLE_CONFIG=deployment-scripts/oracle-framework.config.json \ npm run deploy -- --network localhostSet
FORKED_CHAIN_ID=8453instead for a Base fork. Local-fork deployments use the corresponding upstream RPC for fee selection and cost reporting.To deploy the included BNB Chain configuration, set
BNB_RPC_URLand run:ORACLE_CONFIG=deployment-scripts/oracle-framework.bnb.json \ npm run deploy -- --network bnbFor a local BNB Chain fork, use the same config with
FORKED_CHAIN_ID=56and--network localhost.To deploy the included Arbitrum One configuration, set
ARBITRUM_RPC_URLand run:ORACLE_CONFIG=deployment-scripts/oracle-framework.arbitrum.json \ npm run deploy -- --network arbitrumFor a local Arbitrum One fork, use the same config with
FORKED_CHAIN_ID=42161and--network localhost.To deploy the included Polygon PoS configuration, set
POLYGON_RPC_URLand run:ORACLE_CONFIG=deployment-scripts/oracle-framework.polygon.json \ npm run deploy -- --network polygonFor a local Polygon fork, use the same config with
FORKED_CHAIN_ID=137and--network localhost.To deploy the included OP Mainnet configuration, set
OPTIMISM_RPC_URLand run:ORACLE_CONFIG=deployment-scripts/oracle-framework.optimism.json \ npm run deploy -- --network optimismFor a local OP Mainnet fork, use the same config with
FORKED_CHAIN_ID=10and--network localhost.
The default output is oracle-framework.<network>.json. Set outputFile in the configuration to choose another path.
Existing BNB deployment: USDT configuration
Binance-Peg USDT is configured in the deployed BNB framework at proxy oracle 0x41bcB90dF4533fdb433F3B69b7868DbCf1E9bbBD. The maintenance script encodes the three transactions used for that configuration:
- Register the Chainlink USDT/USD feed in
MinimalChainlinkFeedRegistry. - Configure the 0.01% PancakeSwap V3 USDT/USDC pool in
UniswapV3Oracle. - Create and register a USDT-denominated proxy oracle through
OracleFactory.
Preparation is read-only. It verifies chain ID 56, deployed bytecode, feed metadata and round validity, pool liquidity, current configuration, and all three deployer roles. It then estimates gas, applies the shared fee helper, assigns sequential pending nonces, and writes unsigned transaction data to bnb-usdt-transactions.json:
npm run prepare:bnb-usdtBroadcasting requires a separate explicit command:
npm run configure:bnb-usdtThe execution command stops on the first failed receipt and confirms the USDT oracle registered by the factory. It also refuses to run if any of the three settings already exists, so the deployed BNB configuration cannot be applied twice. Re-run the preparation immediately before execution on a new deployment because nonces and fee fields are time-sensitive.
Contract verification
Set an Etherscan API key to publish source metadata to Etherscan:
ETHERSCAN_API_KEY=...Verify a contract without constructor arguments:
npm run verify -- --network mainnet 0xContractAddressAppend constructor arguments in deployment order for contracts that have them. The verifier uses Etherscan's unified V2 API. Sourcify verification is disabled because the API v1 endpoint supported by Hardhat 2 is in a scheduled service brownout.
Deployment configuration
| Field | Required | Default | Description |
| -------------------------------- | :---------: | --------------- | ------------------------------------------------------------------------ |
| tokens.usdc | Yes | — | USDC address used by Chainlink routing. |
| tokens.weth | Yes | — | Wrapped-native token address. |
| tokens.wbtc | Yes | — | Wrapped-BTC token address. |
| tokens.usdt | No | — | Optional USDT address used by chain-specific explicit configuration. |
| tokens.eurc | Conditional | — | Required only when snapshotting the legacy EURC/USD registry route. |
| tokens.wpol | No | — | Optional wrapped POL address used by the Polygon configuration. |
| tokens.referenceAsset | No | tokens.usdc | Asset in which tokenReferenceValue results are denominated. |
| chainlinkFeedRegistry | Conditional | — | Source registry; mutually exclusive with chainlinkFeeds. |
| chainlinkFeeds | Conditional | — | Explicit feed pairs; mutually exclusive with chainlinkFeedRegistry. |
| uniswapV3Factory | Yes | — | Factory queried for configured Uniswap V3 pools. |
| observationPeriod | Yes | — | TWAP lookback in seconds. |
| observationSlots | No | Unchanged | Increases configured pools' observation cardinality when needed. |
| uniswapV3Pools | No | [] | Pool pairs and raw Uniswap fee tiers to register. |
| existingWrappedNativePriceFeed | No | Deploy new | Reuses a compatible feed after an interrupted deployment. |
| spotRouter | No | Omitted | Deploys the test-only instantaneous SpotOracle. |
| liquidityRegistry | No | Omitted | Deploys and initializes the Balancer V2 adapter when provided. |
| erc4626Vaults | No | [] | Vault-share addresses registered with the ERC-4626 adapter. |
| deployRegistryAndFactory | No | true | Deploys the registry, proxy implementation, and oracle factory. |
| deployBalanceQuoter | No | true | Deploys the quoter when the registry/factory group is enabled. |
| factoryReferenceAssets | No | [] | Additional reference assets for which the factory creates proxy oracles. |
| confirmations | No | 1 | Confirmations awaited for each deployment or configuration transaction. |
| outputFile | No | Network-derived | Deployment-manifest path. |
Uniswap-compatible fee values are raw fee tiers. Ethereum uses tiers including 500, 3000, and 10000; PancakeSwap V3 on BNB Chain uses 100, 500, 2500, and 10000.
The Ethereum example config selects the deepest active standard-fee Uniswap V3 pools for EURC/USDC (500), WETH/USDC (500), WBTC/USDC (3000), WBTC/WETH (500), and EURC/WETH (500). The mainnet-fork deployment test compares each configured pool's active liquidity against the 100, 500, 3000, and 10000 fee tiers so changes in the deepest pool are detected.
The BNB Chain config uses Binance-Peg USDC, USDT, WBNB, and BTCB with USDC as the reference asset. It configures the deepest active PancakeSwap V3 pools at the tested fork block: BTCB/USDC (2500), WBNB/USDC (100), USDT/USDC (100), and BTCB/WBNB (500). Its explicit Chainlink entries use the standard BNB/USD, BTC/USD, USDC/USD, and USDT/USD proxies, while BTCB/BTC and WBNB/native use the deployed fixed 1:1 feed. The BNB fork test rechecks each configured pool against every supported PancakeSwap V3 fee tier.
The Arbitrum One config uses native USDC, USD₮0, WETH, and WBTC with USDC as the reference asset. It configures the deepest active standard Uniswap V3 pools at the tested fork block: WETH/USDC (500), WBTC/USDC (500), USDT/USDC (100), and WBTC/WETH (500). Its explicit Chainlink entries use the standard ETH/USD, BTC/USD, USDC/USD, and USDT/USD proxies, while WETH/native and WBTC/BTC use the deployed fixed 1:1 feed. The Arbitrum fork test validates every feed, rechecks each pool against all standard fee tiers, deploys the complete registry/factory stack, and prices USDT, WETH, and WBTC through both fallback sources.
The Polygon PoS config uses native USDC, USDT0, WPOL, WETH, and WBTC with USDC as the reference asset. It configures the deepest active standard Uniswap V3 pools found at the tested block: USDT/USDC (100), WPOL/USDC (500), WETH/USDC (500), WBTC/USDC (500), WBTC/WETH (500), WPOL/WETH (3000), and USDT/WETH (500). Its explicit Chainlink entries use the USDC/USD, USDT/USD, POL/USD, ETH/USD, and BTC/USD proxies, while WETH/native and WBTC/BTC use the deployed fixed 1:1 feed. The Polygon fork test validates every feed, rechecks each pool against all standard fee tiers, deploys the complete registry/factory stack, and prices USDT, WPOL, WETH, and WBTC through both fallback sources.
The OP Mainnet config uses native USDC, USDT, WETH, and WBTC with USDC as the reference asset. It configures the deepest active standard Uniswap V3 pools found at the tested block: USDT/USDC (100), WETH/USDC (3000), WBTC/USDC (3000), WBTC/WETH (3000), and USDT/WETH (500). Its explicit Chainlink entries use the USDC/USD, USDT/USD, ETH/USD, and BTC/USD proxies, while WETH/native and WBTC/BTC use the deployed fixed 1:1 feed. The Optimism fork test validates every feed, rechecks each pool against all standard fee tiers, deploys the complete registry/factory stack, and prices USDT, WETH, and WBTC through both fallback sources.
The deployer can either read ETH/USD, USDC/USD, WBTC/BTC, BTC/USD, and EURC/USD from chainlinkFeedRegistry, or consume an explicit chainlinkFeeds array. An explicit entry can provide oracle and decimals, or set fixedOneToOne: true to use the deployed 18-decimal 1:1 feed. All pairs are registered in a new MinimalChainlinkFeedRegistry. ChainlinkOracle references the deployed minimal registry, so runtime feed detection does not depend on the source implementing ExtFeedRegistryInterface.
The base ReferenceAssetOracle is registered directly for tokens.referenceAsset. USDC is always configured in OracleRegistry as well: when USDC is not the base reference asset, the factory creates and registers a USDC-denominated proxy oracle. Duplicate base or factory reference assets are ignored.
Deployment order
The deployer performs these steps:
- Read feed addresses and decimals from the configured Chainlink source registry, or validate the explicit feed list.
- Deploy
WrappedNativePriceFeed, deploy and initializeMinimalChainlinkFeedRegistry, then deployChainlinkOracleagainst it. - Deploy the optional spot source and deploy and configure the Uniswap V3 source.
- Deploy the optional Balancer adapter and the ERC-4626 adapter.
- Initialize the ERC-4626 adapter's reference asset, then deploy
ReferenceAssetOraclewith ERC-4626 as its preferred source. - Initialize the adapters against
ReferenceAssetOracle. - Configure Chainlink and Uniswap V3 as the first and second fallbacks.
- Optionally deploy the registry, proxy implementation, factory, proxy oracles, and balance quoter.
- Write all deployed addresses to the manifest.
Access control
The framework uses OpenZeppelin AccessControl:
ReferenceAssetOracle.ORACLE_ADMINmanages fallbacks and paused sources.ChainlinkOracle.ORACLE_ADMINmanages the stale-price period.UniswapV3Oracle.ORACLE_ADMINmanages fee tiers and the minimum-liquidity threshold.- Adapter registration and initialization use
DEFAULT_ADMIN_ROLE. OracleFactory,OracleRegistry, andBalanceQuoteradministration usesDEFAULT_ADMIN_ROLE.
Plan role ownership, multisig control, and role handoff before production deployment.
Security considerations
This repository has not been independently audited. Review and test every deployment configuration before using it with production funds.
SpotOracleconsumes instantaneous router quotes and is manipulable. It is retained for tests and controlled environments only.- Chainlink feed addresses, denominations, decimal precision, and staleness settings must be correct for the target chain. The default stale period is 14 days.
- Uniswap V3 security depends on a suitable observation period, adequate observation cardinality, correct fee-tier selection, and a meaningful minimum-liquidity threshold.
- The Balancer adapter assumes registered compatible pools whose token arrays include the pool token at index zero; it intentionally skips that first entry when computing TVL.
- ERC-4626 pricing trusts the registered vault's
asset()andconvertToAssets()implementations. Register only reviewed vaults. - Adapter fallbacks call the aggregate oracle for underlying prices. Avoid configurations that can recurse without reaching a valid primitive price source.
- A
(0, 0)result means no valid price was available. Integrations must reject it rather than treating it as a legitimate zero-valued asset. - Protect deployer keys and administrative roles. Never commit
.envor deployment secrets.
Repository layout
contracts/
interfaces/ framework and minimal third-party interfaces
libraries/ minimal Uniswap quote library
oracles/ production Oracle contracts
test/ local test doubles and test-only SpotOracle
utils/ cloning and input checks
deployment-scripts/
deploy-oracle-framework.ts
oracle-framework.config.example.json
test/
oracle-framework.test.ts
oracle-framework.integration.test.ts
unit/ deterministic local coverage suites
core/oracles/ copied Ethereum and Base fork suites
utils/module_oracleFramework.tsOrigin
The production Oracle sources and integration tests were extracted from DynaVaults. See ORIGIN.md for the source commit, copied paths, dependency reductions, and deliberate test adaptations.
License
MIT. See LICENSE. Individual vendored or extracted files retain their own SPDX identifiers where applicable.
