On this page
Omnilink data adapters
The Omnilink data adapters turn approved price, NAV, and vault sources into compact typed snapshots that can be relayed as one deterministic read.
They are source-side view contracts. A relayer calls Omnilink.sendRead with the adapter as the target; the adapter itself never sends a cross-chain message.
Shared registration model
Every adapter is Ownable2Step and uses an append-only registration:
- The owner binds an asset or vault to its source once.
- The binding starts enabled.
- The owner can pause and resume it.
- The source address and interpretation cannot be replaced under the existing asset argument.
- A materially different source requires a new adapter/version and a new Omnilink read path.
Every snapshot includes SCHEMA_VERSION = 1 and an immutable configHash. Destination consumers should compare both with their approved configuration before using a result.
This prevents readPrice(asset) or readNav(asset) from silently changing meaning while retaining the same Omnilink calldata hash.
Chainlink price adapter
OmnilinkChainlinkPriceAdapter maps one token address to one approved Chainlink AggregatorV3Interface proxy and a configured quote identifier.
Configuration
struct PriceConfig {
address feed;
bytes32 quoteId;
bytes32 configHash;
uint64 maxAge;
uint8 decimals;
bool enabled;
bool exists;
}
mapping(address asset => PriceConfig config) public priceConfigs;Use a stable quote identifier such as keccak256("USD"). The identifier is part of the immutable config hash; it is semantic metadata, not a token transfer address.
function registerAsset(
address asset,
address feed,
bytes32 quoteId,
uint64 maxAge
) external onlyOwner returns (bytes32 configHash);
function setAssetEnabled(address asset, bool enabled)
external onlyOwner;The asset and feed must both be deployed contracts. Registration stores the proxy's decimals and cannot be repeated for that asset.
Snapshot
struct PriceSnapshot {
uint8 schemaVersion;
address asset;
bytes32 quoteId;
address feed;
bytes32 configHash;
uint64 maxAge;
uint8 decimals;
uint80 roundId;
int256 answer;
uint256 startedAt;
uint256 updatedAt;
uint80 answeredInRound;
}
function readPrice(address asset)
external view returns (PriceSnapshot memory snapshot);One read calls decimals() and latestRoundData() on the configured proxy. It rejects:
- Unregistered or disabled assets.
- A missing round or zero update timestamp.
- A nonpositive price.
- A feed timestamp in the future.
- A result older than the configured
maxAge. - Feed decimals that differ from registration.
answeredInRound is retained as raw Chainlink round metadata. New consumers should not use it as their freshness check; use updatedAt and the configured policy.
Chainlink NAV adapter
OmnilinkChainlinkNavAdapter supports single-value Chainlink NAV feeds that implement AggregatorV3Interface.
Multiple-Variable Response feeds use latestBundle() and are intentionally outside this adapter's schema.
NAV semantics
enum NavType {
None,
PerShare,
Total
}PerShare means the answer values one asset/share unit. Total means the answer reports the whole vehicle or portfolio NAV. Consumers must not treat these as interchangeable.
Configuration
struct NavConfig {
address feed;
bytes32 currencyId;
bytes32 configHash;
uint64 maxAge;
uint8 decimals;
NavType navType;
bool enabled;
bool exists;
}
mapping(address asset => NavConfig config) public navConfigs;
function registerAsset(
address asset,
address feed,
bytes32 currencyId,
NavType navType,
uint64 maxAge
) external onlyOwner returns (bytes32 configHash);
function setAssetEnabled(address asset, bool enabled)
external onlyOwner;The currency identifier, NAV type, max age, feed, and decimals all form part of configHash.
Snapshot
struct NavSnapshot {
uint8 schemaVersion;
address asset;
bytes32 currencyId;
address feed;
bytes32 configHash;
uint64 maxAge;
uint8 decimals;
NavType navType;
uint80 roundId;
int256 answer;
uint256 startedAt;
uint256 updatedAt;
uint80 answeredInRound;
}
function readNav(address asset)
external view returns (NavSnapshot memory snapshot);The round and metadata checks match the price adapter. Configure maxAge for the NAV's actual publication schedule and market-hours behavior; it will often be longer than a continuously updated crypto price feed.
ERC-4626 adapter
OmnilinkERC4626Adapter treats the vault share token address as the requested asset and returns its conversion and accounting context from one source block.
Configuration
struct VaultConfig {
address underlying;
bytes32 configHash;
uint8 shareDecimals;
uint8 assetDecimals;
bool enabled;
bool exists;
}
mapping(address vault => VaultConfig config) public vaultConfigs;
function registerVault(
address vault,
address expectedUnderlying
) external onlyOwner returns (bytes32 configHash);
function setVaultEnabled(address vault, bool enabled)
external onlyOwner;Registration verifies vault.asset() against expectedUnderlying and stores both decimal values. Share decimals above 77 are rejected because the adapter computes one full share as 10 ** shareDecimals.
Snapshot
struct VaultSnapshot {
uint8 schemaVersion;
address vault;
address underlying;
bytes32 configHash;
uint8 shareDecimals;
uint8 assetDecimals;
uint256 unitShares;
uint256 assetsPerUnit;
uint256 totalAssets;
uint256 totalSupply;
}
function readVault(address vault)
external view returns (VaultSnapshot memory snapshot);The aggregate performs:
unitShares = 10 ** vault.decimals();
assetsPerUnit = vault.convertToAssets(unitShares);
underlying = vault.asset();
totalAssets = vault.totalAssets();
totalSupply = vault.totalSupply();It rejects underlying or decimal drift and a zero convertToAssets result.
convertToAssets is an accounting estimate, not an independent price oracle. Donation/inflation behavior, losses, virtual-share implementations, upgradeability, and protocol-specific accounting remain consumer risks. A destination integration should apply freshness and deviation controls and should not assume that the rate can only increase.
Events
event PriceAssetRegistered(
address indexed asset,
address indexed feed,
bytes32 indexed quoteId,
uint64 maxAge,
uint8 decimals,
bytes32 configHash
);
event PriceAssetEnabledChanged(address indexed asset, bool enabled);
event NavAssetRegistered(
address indexed asset,
address indexed feed,
bytes32 indexed currencyId,
NavType navType,
uint64 maxAge,
uint8 decimals,
bytes32 configHash
);
event NavAssetEnabledChanged(address indexed asset, bool enabled);
event VaultRegistered(
address indexed vault,
address indexed underlying,
uint8 shareDecimals,
uint8 assetDecimals,
bytes32 configHash
);
event VaultEnabledChanged(address indexed vault, bool enabled);Error groups
| Adapter | Error conditions |
|---|---|
| Price | Invalid address/config, duplicate or missing asset, disabled asset, invalid answer/round, stale/future timestamp, decimal drift |
| NAV | Invalid address/config or NavType.None, duplicate or missing asset, disabled asset, invalid answer/round, stale/future timestamp, decimal drift |
| ERC-4626 | Invalid address/config, duplicate or missing vault, disabled vault, underlying drift, decimal drift, zero conversion |
Configure the Omnilink path
Register every adapter call separately on the source Omnilink:
bytes memory callData = abi.encodeCall(
priceAdapter.readPrice,
(asset)
);
omnilink.configureRead(
address(priceAdapter),
callData,
address(0),
500_000,
512,
true
);Using the adapter as Omnilink's target keeps the aggregate atomic while preserving the external sendRead caller as the bounty recipient. Continue with Relay a read with Omnilink.