Storage Operations

Overview

Contract storage can be read directly using storage pointers, providing low-level access to contract state without invoking contract functions. The provider exposes the getStorageAt() method to retrieve raw storage values at specific pointer locations. This capability is useful for debugging, state verification, building block explorers, and advanced integrations that require direct access to contract internals.

Reading Contract Storage

Using getStorageAt()

The getStorageAt() method retrieves a value from a contract's storage at a specific pointer. The method accepts the contract address, a storage pointer as bigint or base64 string, an optional flag to include Merkle proofs, and an optional block height for historical queries.

The returned StoredValue object contains the raw storage data and optional proof information for verification. This low-level access is useful for debugging, building explorers, or accessing storage slots not exposed through the contract's public interface.

Method Signature

typescript
getStorageAt() signature
async getStorageAt(
    address: string | Address,     // Contract address
    rawPointer: bigint | string,   // Storage pointer (bigint or base64)
    proofs?: boolean,              // Include proofs (default: true)
    height?: BigNumberish          // Optional block height
): Promise<StoredValue>

StoredValue Structure

typescript
StoredValue structure
interface StoredValue {
    pointer: bigint;       // Storage slot pointer
    value: Uint8Array;     // Stored value as Uint8Array
    height: bigint;        // Block height of the value
    proofs: string[];      // Merkle proofs for verification
}

Basic Storage Query

The following example demonstrates retrieving a storage value using a numeric pointer. The returned object contains the pointer, raw value as bytes, and the block height at which the value was read.

typescript
Basic storage query
import { JSONRpcProvider } from 'opnet';
import { networks, toHex } from '@btc-vision/bitcoin';

const network = networks.regtest;
const provider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network });

const contractAddress = 'bc1p...contract-address...';
const pointer = 123456789n; // Storage slot as bigint

const storage = await provider.getStorageAt(contractAddress, pointer);

console.log('Storage Value:');
console.log('  Pointer:', storage.pointer);
console.log('  Value:', toHex(storage.value));
console.log('  Height:', storage.height);

With Base64 String Pointer

Storage pointers can also be provided as base64-encoded strings, which is useful when working with pointer values obtained from external sources or APIs that return pointers in this format.

typescript
With string pointer
// Pointer can be provided as base64 string
const base64Pointer = 'EXLK/QhEQMI5d9DrthLvozT+UcDQ7WuSPaz7g8GV3AQ=';

const storage = await provider.getStorageAt(
    contractAddress,
    base64Pointer
);

console.log('Value:', toHex(storage.value));

With/Without Proofs

By default, storage queries include Merkle proofs for cryptographic verification of the returned value. Setting the proofs parameter to false skips proof generation, resulting in faster responses when verification is not required.

typescript
With/Without proofs
// Request proofs for verification (default: true)
const storage = await provider.getStorageAt(
    contractAddress,
    pointer,
    true  // Include proofs
);

console.log('Proofs:', storage.proofs.length);
for (const proof of storage.proofs) {
    console.log('  Proof:', proof
}

// Skip proofs for faster response
const storage2 = await provider.getStorageAt(
    contractAddress,
    pointer,
    false  // No proofs
);

console.log('Value:', toHex(storage2.value));
// storage2.proofs will be empty

At Specific Height

Storage values can be retrieved at a specific block height, enabling historical state analysis and point-in-time queries. The returned height confirms the block at which the value was read.

typescript
At specific height
// Read historical storage at specific block height
const historicalStorage = await provider.getStorageAt(
    contractAddress,
    pointer,
    true,     // Include proofs
    100000n   // Block height
);

console.log('Value at block 100000:', toHex(historicalStorage.value));
console.log('Confirmed at height:', historicalStorage.height);

Decoding Stored Values

Raw storage values are returned as byte arrays and require decoding based on the expected data type. Common storage types include integers, addresses, and strings, each requiring different decoding logic. The following examples demonstrate decoding storage values into their typed representations.

Decoding Uint256

Integer storage values are typically stored as 32-byte big-endian unsigned integers. The following example demonstrates decoding raw bytes into a bigint value.

typescript
Convert to BigInt
function decodeUint256(value: Uint8Array): bigint {
    // Stored values are typically 32 bytes
    if (value.length === 0) return 0n;

    // Read as big-endian unsigned integer
    let result = 0n;
    for (const byte of value) {
        result = (result << 8n) | BigInt(byte);
    }
    return result;
}

// Usage
const storage = await provider.getStorageAt(contractAddress, pointer);
const balance = decodeUint256(storage.value);
console.log('Balance:', balance);

Decoding Address

Address values are stored as byte sequences and can be extracted from the raw storage data. The following example demonstrates decoding storage bytes into an Address object.

typescript
Convert to BigInt
import { Address } from '@btc-vision/bitcoin';

function decodeAddress(value: Uint8Array): Address | null {
    if (value.length < 20) return null;

    // Extract address bytes
    const addressBytes = value.slice(-20);
    return Address.fromBuffer(addressBytes);
}

// Usage
const storage = await provider.getStorageAt(contractAddress, ownerSlot);
const owner = decodeAddress(storage.value);
console.log('Owner:', owner?.toHex());

Reading Map Value

Contract mappings store values at computed storage slots derived from the base slot and key. The pointer is calculated by hashing the concatenation of the key and slot using keccak256. The following example demonstrates reading a value from a balances mapping by computing its storage pointer.

typescript
Convert to BigInt
import { keccak256 } from 'ethers';
import { fromHex } from '@btc-vision/bitcoin';

function getMapPointer(mapSlot: bigint, key: Uint8Array): bigint {
    // Standard mapping pointer calculation
    const slotBuffer = new Uint8Array(32);
    new DataView(slotBuffer.buffer).setBigUint64(24, mapSlot);

    const data = new Uint8Array([...key, ...slotBuffer]);
    const hash = keccak256(data);

    return BigInt(hash);
}

// Usage: Read balances[address]
const balancesSlot = 0n; // Storage slot of balances mapping
const userKey = fromHex(userAddress.slice(2));
const pointer = getMapPointer(balancesSlot, userKey);

const storage = await provider.getStorageAt(contractAddress, pointer);
console.log('User balance:', decodeUint256(storage.value));

Verifying Storage Proofs

Storage queries return Merkle proofs that enable cryptographic verification of the returned value without trusting the provider. Verifying these proofs confirms that the storage value is consistent with the blockchain state at the specified block height, providing strong integrity guarantees for critical operations.

Verify Storage Proof

The following example demonstrates verifying a storage value by comparing it against an expected value and confirming that proofs are present. For full cryptographic verification, additional logic would validate the Merkle proof against the block's state root.

typescript
Verify Storage Proof
async function verifyStorageWithProof(
    provider: JSONRpcProvider,
    contractAddress: string,
    pointer: bigint,
    expectedValue: Uint8Array
): Promise<boolean> {
    const storage = await provider.getStorageAt(
        contractAddress,
        pointer,
        true  // Request proofs
    );

    // Compare value
    if (storage.value.length !== expectedValue.length ||
        !storage.value.every((b, i) => b === expectedValue[i])) {
        return false;
    }

    // Verify proofs exist
    if (storage.proofs.length === 0) {
        console.warn('No proofs provided');
        return false;
    }

    // Additional proof verification would go here
    // (depends on specific proof format)

    return true;
}

// Usage
const verified = await verifyStorageWithProof(
    provider,
    contractAddress,
    pointer,
    expectedBytes
);
console.log('Storage verified:', verified);

Compare Storage Values

Comparing storage values at different block heights enables detection of state changes over time. The following example queries storage at two block heights and compares the values to determine if a change occurred within the specified range.

typescript
Compare storage values
async function hasStorageChanged(
    provider: JSONRpcProvider,
    contractAddress: string,
    pointer: bigint,
    fromHeight: bigint,
    toHeight: bigint
): Promise<boolean> {
    const [oldStorage, newStorage] = await Promise.all([
        provider.getStorageAt(contractAddress, pointer, false, fromHeight),
        provider.getStorageAt(contractAddress, pointer, false, toHeight),
    ]);

    return oldStorage.value.length !== newStorage.value.length ||
        !oldStorage.value.every((b, i) => b === newStorage.value[i]);
}

// Usage
const changed = await hasStorageChanged(
    provider,
    contractAddress,
    pointer,
    100000n,
    100100n
);
console.log('Storage changed:', changed);

Reading Multiple Storage Slots

Reading multiple storage slots individually results in separate network requests for each query. Using concurrent queries with Promise.all() improves efficiency by fetching multiple values in parallel, reducing overall latency when accessing several storage locations from the same contract.

Read Multiple Storage Slots

typescript
Read multiple slots
async function getMultipleStorage(
    provider: JSONRpcProvider,
    contractAddress: string,
    pointers: bigint[]
): Promise<Map<bigint, Uint8Array>> {
    const results = new Map<bigint, Uint8Array>();

    // Read in parallel
    const promises = pointers.map(pointer =>
        provider.getStorageAt(contractAddress, pointer, false)
    );

    const storageValues = await Promise.all(promises);

    for (let i = 0; i < pointers.length; i++) {
        results.set(pointers[i], storageValues[i].value);
    }

    return results;
}

// Usage
const pointers = [0n, 1n, 2n, 3n]; // Multiple storage slots
const values = await getMultipleStorage(provider, contractAddress, pointers);

for (const [pointer, value] of values) {
    console.log(`Slot ${pointer}: ${toHex(value)}`);
}

Read Storage Range

typescript
Read storage range
async function getStorageRange(
    provider: JSONRpcProvider,
    contractAddress: string,
    startPointer: bigint,
    count: number
): Promise<StoredValue[]> {
    const pointers = Array.from(
        { length: count },
        (_, i) => startPointer + BigInt(i)
    );

    const promises = pointers.map(pointer =>
        provider.getStorageAt(contractAddress, pointer, false)
    );

    return Promise.all(promises);
}

// Usage
const storageRange = await getStorageRange(provider, contractAddress, 0n, 10);
console.log('First 10 storage slots:');
for (const storage of storageRange) {
    console.log(`  ${storage.pointer}: ${toHex(storage.value)}`);
}

Complete Storage Service

typescript
Storage service
class StorageService {
    constructor(private provider: JSONRpcProvider) {}

    async get(
        contract: string,
        pointer: bigint,
        options?: { proofs?: boolean; height?: bigint }
    ): Promise<StoredValue> {
        return this.provider.getStorageAt(
            contract,
            pointer,
            options?.proofs ?? false,
            options?.height
        );
    }

    async getValue(contract: string, pointer: bigint): Promise<Uint8Array> {
        const storage = await this.get(contract, pointer);
        return storage.value;
    }

    async getUint256(contract: string, pointer: bigint): Promise<bigint> {
        const value = await this.getValue(contract, pointer);
        return this.decodeUint256(value);
    }

    async getMultiple(
        contract: string,
        pointers: bigint[]
    ): Promise<Map<bigint, Uint8Array>> {
        const results = new Map<bigint, Uint8Array>();

        const storageValues = await Promise.all(
            pointers.map(p => this.get(contract, p))
        );

        for (let i = 0; i < pointers.length; i++) {
            results.set(pointers[i], storageValues[i].value);
        }

        return results;
    }

    async hasValue(contract: string, pointer: bigint): Promise<boolean> {
        const value = await this.getValue(contract, pointer);
        return value.length > 0 && !value.every(b => b === 0);
    }

    async compare(
        contract: string,
        pointer: bigint,
        height1: bigint,
        height2: bigint
    ): Promise<{
        changed: boolean;
        value1: Uint8Array;
        value2: Uint8Array;
    }> {
        const [s1, s2] = await Promise.all([
            this.get(contract, pointer, { height: height1 }),
            this.get(contract, pointer, { height: height2 }),
        ]);

        return {
            changed: s1.value.length !== s2.value.length ||
                !s1.value.every((b, i) => b === s2.value[i]),
            value1: s1.value,
            value2: s2.value,
        };
    }

    private decodeUint256(value: Uint8Array): bigint {
        if (value.length === 0) return 0n;

        let result = 0n;
        for (const byte of value) {
            result = (result << 8n) | BigInt(byte);
        }
        return result;
    }
}

// Usage
const storageService = new StorageService(provider);

// Read single value
const balance = await storageService.getUint256(contractAddress, balanceSlot);
console.log('Balance:', balance);

// Read multiple
const values = await storageService.getMultiple(contractAddress, [0n, 1n, 2n]);
for (const [slot, value] of values) {
    console.log(`Slot ${slot}:`, toHex(value));
}

// Compare historical values
const comparison = await storageService.compare(
    contractAddress,
    pointer,
    100000n,
    100500n
);
console.log('Value changed:', comparison.changed);

Common Storage Patterns

ERC-20 Style Storage Layout

Token contracts follow common storage layouts where standard properties occupy predictable slots. The following example defines common slot positions for ERC-20-like contracts and demonstrates fetching multiple token properties concurrently using parallel queries.

typescript
ERC-20 style storage layout
// Common storage slots for ERC-20-like contracts
const STORAGE_SLOTS = {
    NAME: 0n,           // string name
    SYMBOL: 1n,         // string symbol
    DECIMALS: 2n,       // uint8 decimals
    TOTAL_SUPPLY: 3n,   // uint256 totalSupply
    BALANCES: 4n,       // mapping(address => uint256)
    ALLOWANCES: 5n,     // mapping(address => mapping(address => uint256))
};

async function getTokenInfo(
    provider: JSONRpcProvider,
    tokenAddress: string
): Promise<{
    totalSupply: bigint;
    decimals: bigint;
}> {
    const storageService = new StorageService(provider);

    const [totalSupply, decimals] = await Promise.all([
        storageService.getUint256(tokenAddress, STORAGE_SLOTS.TOTAL_SUPPLY),
        storageService.getUint256(tokenAddress, STORAGE_SLOTS.DECIMALS),
    ]);

    return { totalSupply, decimals };
}

Best Practices

  1. Use Pointers Correctly: Ensure the correct storage slot or pointer is used for the data being accessed. Incorrect pointers return unrelated or empty values without error.
  2. Skip Proofs When Unnecessary: Set proofs to false for faster queries when cryptographic verification is not required. Proof generation adds overhead to each request.
  3. Batch Requests: Read multiple storage values in parallel using Promise.all() to reduce overall latency when accessing several slots.
  4. Handle Empty Values: Storage slots may be empty or zero-filled if never written. Check for empty arrays or zero values before decoding.
  5. Historical Queries: Use the height parameter for point-in-time reads when analyzing past state or debugging historical transactions.
  6. Cache Results: Storage values at specific block heights are immutable and can be cached indefinitely to reduce redundant queries.