Skip to content
AtlasOracle

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.

solidity
// 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

bash
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

bash
npm install @atlas-oracle/pull-oracle-consumer
ts
// 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.

CapabilityStandardStandard StorageTransientTransient Storage
Requires Cancun (EIP-1153)NoNoYesYes
Runtime config changesNoYesNoYes
Zero SLOAD overheadYesNoYesNo
Min Solidity0.8.130.8.130.8.240.8.24
Batch complexityO(M×N)O(M×N)O(M+N)O(M+N)
Config sourceHardcodedStorageHardcodedStorage

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.

FunctionModeMissing feedComplexityMutability
_getVerifiedFeedDataSingleRevertO(N)view
_getVerifiedFeedDataLenientSingleReturns (0,0)O(N)view
_getVerifiedFeedDataBatchBatchRevertO(M×N)view
_getVerifiedFeedDataBatchLenientBatchReturns (0,0)O(M×N)view
_getVerifiedFeedDataBatchTransientBatchRevertO(M+N)non-view
_getVerifiedFeedDataBatchLenientTransientBatchReturns (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.

HookResponsibility
_validateTimestampEnforce price freshness — revert if the timestamp is too stale or too far in the future
_getMaxPackageCountCap the number of feed packages per call to bound gas consumption
_isAuthorizedSignerVerify the recovered ECDSA signer is authorized to provide price data

Reference values (hardcoded variants)

ParameterReference valueDescription
maxDelay180 secondsPrice data older than this is rejected as stale
maxFutureDrift60 secondsTolerance for clock skew (future timestamps)
maxPackageCount255Maximum feed packages per call
authorizedSigner0x59eD4701224fD9e2a85Ef2946c2ab828C1dDC600Atlas 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.

solidity
/// @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

PracticeRecommendation
Signer validationAlways verify the authorized signer matches the official Atlas Oracle address
Freshness boundsSet a reasonable time validation boundary based on the nature of your project
Access controlGate all storage setters behind robust access control (timelock recommended)
Feed ID validationUse constants — never accept user-supplied feed IDs in production
Price sanity checksConsider application-level bounds checks on returned prices