Integrate the Pull Mode
In Pull mode, AtlasOracle delivers cryptographically signed price data through transaction calldata rather than storing it on-chain. Your off-chain application fetches a signed payload from the Atlas service and appends it to your transaction; your contract verifies and reads the price at the point of use. This suits applications that want the freshest possible price without relying on a stored on-chain value.
Pull mode is stateless — no on-chain storage is written or read during verification. Signature recovery, timestamp validation, and price decoding all run on calldata and transient computation, giving very low gas cost with zero storage overhead per query, and eliminating stale-price risk.
Under Pull mode we provide four consumer contracts:
Standard and Standard Storage — work on any EVM ≥ Paris. Standard uses compile-time constants (zero SLOAD); Standard Storage adds runtime-configurable governance.
Transient and Transient Storage — require Cancun (EIP-1153). They use transient storage for efficient large batches; the Storage variant adds runtime governance.
1. How It Works
Signed price data rides along at the tail of your normal transaction calldata. Your function signatures, selectors, and parameters stay completely unchanged — the consumer contract parses prices from the end of the calldata.
Your off-chain application uses the TypeScript SDK to fetch signed oracle data from the Atlas service.
The SDK appends the signed payload to the end of your normal transaction calldata.
Your contract calls
_getVerifiedFeedData(feedId)(or a batch variant) — it parses the trailing data, recovers the signer, validates freshness, and returns the verified price.
Prices are 18-decimal fixed-point
Returned as uint256 with 18-decimal precision (e.g. 67000e18 = $67,000), directly compatible with standard DeFi math — price * amount / 1e18.
2. Quick Start
Inherit a consumer variant and call the accessor inside your business logic. The single-feed call reverts if the feed is missing or stale; the batch call verifies one signature for all feeds.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {PullOracleConsumerStandard} from "pull-oracle-consumer/PullOracleConsumerStandard.sol";
contract MyProtocol is PullOracleConsumerStandard {
// Feed IDs are assigned by Atlas Oracle — check the feed registry
bytes4 internal constant BTC_USD = 0x00000001;
bytes4 internal constant ETH_USD = 0x00000002;
function settle() external {
// Single feed — reverts if missing or stale
(uint256 btcPrice, uint256 timestamp) = _getVerifiedFeedData(BTC_USD);
}
function multiSettle() external {
bytes4[] memory feedIds = new bytes4[](2);
feedIds[0] = BTC_USD;
feedIds[1] = ETH_USD;
// Batch — one signature verification for all feeds
(uint256[] memory prices, uint256[] memory ts) = _getVerifiedFeedDataBatch(feedIds);
}
}Calldata requirement
The transaction calling settle() must include the signed oracle payload appended to its calldata. This is handled automatically by the TypeScript SDK.
Contract example:
https://github.com/oracle-atlas/pull-oracle-consumer/tree/main/examples
Contract interface SDK:
https://github.com/oracle-atlas/pull-oracle-consumer
3. Installation
Foundry (recommended)
forge install oracle-atlas/pull-oracle-consumer
# remappings.txt
pull-oracle-consumer/=lib/pull-oracle-consumer/src/
# Transient (EIP-1153) variants also need:
pull-oracle-consumer-advanced/=lib/pull-oracle-consumer/src-advanced/Hardhat / Node.js
npm install @atlas-oracle/pull-oracle-consumer// hardhat.config.ts — compilers
solidity: {
compilers: [
{ version: "0.8.13", settings: { evmVersion: "paris" } },
// Only for Transient variants:
{ version: "0.8.24", settings: { evmVersion: "cancun" } },
],
}4. Choose Your Variant
Pick based on your chain's EVM version, whether you need runtime-configurable governance, and your expected batch sizes.
| Capability | Standard | Standard Storage | Transient | Transient Storage |
|---|---|---|---|---|
| Requires Cancun (EIP-1153) | No | No | Yes | Yes |
| Runtime config changes | No | Yes | No | Yes |
| Zero SLOAD overhead | Yes | No | Yes | No |
| Min Solidity | 0.8.13 | 0.8.13 | 0.8.24 | 0.8.24 |
| Batch complexity | O(M×N) | O(M×N) | O(M+N) | O(M+N) |
| Config source | Hardcoded | Storage | Hardcoded | Storage |
O(M+N) is not automatically cheaper
The Transient variant caches all N payload feeds with TSTORE, then does M lookups — a fixed (2N + M) × 100 gas overhead regardless of how few feeds you request. The Standard variant searches at only ~3 gas per CALLDATALOAD, up to M × N × 3 total.
For M=2 requested and N=5 payload feeds: Transient = (2×5+2)×100 = 1,200 gas; Standard = 2×5×3 = 30 gas. Transient only wins once M×N is large. If you're gas-sensitive, benchmark both with your real feed configuration.
5. Data Accessor Functions
Inheriting a consumer variant gives your contract these internal functions. Each performs signature verification, timestamp validation, and price decoding in one step — with no external calls.
| Function | Mode | Missing feed | Complexity | Mutability |
|---|---|---|---|---|
_getVerifiedFeedData | Single | Revert | O(N) | view |
_getVerifiedFeedDataLenient | Single | Returns (0,0) | O(N) | view |
_getVerifiedFeedDataBatch | Batch | Revert | O(M×N) | view |
_getVerifiedFeedDataBatchLenient | Batch | Returns (0,0) | O(M×N) | view |
_getVerifiedFeedDataBatchTransient | Batch | Revert | O(M+N) | non-view |
_getVerifiedFeedDataBatchLenientTransient | Batch | Returns (0,0) | O(M+N) | non-view |
Strict vs lenient
Strict functions revert with UnmatchedFeedID if a requested feed isn't in the payload. Lenient functions return (0, 0) instead — to check presence, test aggregatedTimestamp != 0 rather than price != 0. Transient batch functions use TSTORE internally and cannot be called via staticcall.
6. Configuration & Hooks
Three virtual hooks control your security policy; every accessor call invokes them internally. Hardcoded variants override them with compile-time constants (zero SLOAD); storage-backed variants read from contract storage, configurable at runtime via governance.
| Hook | Responsibility |
|---|---|
_validateTimestamp | Enforce price freshness — revert if the timestamp is too stale or too far in the future |
_getMaxPackageCount | Cap the number of feed packages per call to bound gas consumption |
_isAuthorizedSigner | Verify the recovered ECDSA signer is authorized to provide price data |
Reference values (hardcoded variants)
| Parameter | Reference value | Description |
|---|---|---|
maxDelay | 180 seconds | Price data older than this is rejected as stale |
maxFutureDrift | 60 seconds | Tolerance for clock skew (future timestamps) |
maxPackageCount | 255 | Maximum feed packages per call |
authorizedSigner | 0x59eD4701224fD9e2a85Ef2946c2ab828C1dDC600 | Atlas Oracle production signing key |
Override the references
We recommend overriding these hooks with values tailored to your protocol — acceptable staleness for your asset class, expected payload size, and your signer-management strategy — rather than relying on the reference defaults.
/// @dev Tighten freshness to 60s, drift to 30s; limit packages; pin signer
function _getMaxPackageCount() internal view override returns (uint256) {
return 10;
}
function _isAuthorizedSigner(address signer) internal view override returns (bool) {
// Override after a signer rotation — see Atlas docs for the current key
return signer == 0x1234567890AbcdEF1234567890aBcdef12345678;
}Storage-backed governance
The Storage variants store configuration on-chain, changeable without redeployment. Expose the internal setters — _setSignerStatus, _setMaxDelay, _setMaxFutureDrift, _setMaxPackageCount — behind your access control (Ownable, AccessControl, or a timelock).
Empty signer set locks the contract
An empty initialSigners array is valid for two-step deployment, but you must expose _setSignerStatus through an access-controlled function — otherwise the contract permanently rejects all oracle payloads.
7. Security Considerations
The SDK has been audited by CertiK. Two behaviors deserve special attention in production.
Price-data replay awareness
The stateless payload contains no msg.sender and no per-use nonce. The same signed blob can be reused by any address while its timestamps remain within the freshness window, and since calldata is public, observers can copy it from the mempool and replay it to the same contract. Within the window, multiple valid prices may coexist, so an observer could select the most favorable one.
If your protocol is price-selection sensitive (lending, staking, liquidation), add safeguards: monotonic timestamp checks, per-user cooldowns, or access-controlled entry points.
Transient storage slot conflict
When using the transient batch functions, do not perform any TSTORE to slots derived from feed-ID values in the same transaction. The reserved key space is bytes4(feedId) stored left-aligned as bytes32. Any TSTORE to that form conflicts with the lookup cache and may return incorrect prices.
Best practices
| Practice | Recommendation |
|---|---|
| Signer validation | Always verify the authorized signer matches the official Atlas Oracle address |
| Freshness bounds | Set a reasonable time validation boundary based on the nature of your project |
| Access control | Gate all storage setters behind robust access control (timelock recommended) |
| Feed ID validation | Use constants — never accept user-supplied feed IDs in production |
| Price sanity checks | Consider application-level bounds checks on returned prices |