home / skills / multiversx / mx-ai-skills / multiversx-cross-contract-storage

multiversx-cross-contract-storage skill

/skills/multiversx-cross-contract-storage

This skill reads another contract's storage directly to avoid proxy calls and reduce gas when querying same-shard state.

npx playbooks add skill multiversx/mx-ai-skills --skill multiversx-cross-contract-storage

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.2 KB
---
name: multiversx-cross-contract-storage
description: Read another contract's storage directly without async calls using storage_mapper_from_address. Use when building aggregators, controllers, or any contract that needs to read state from other same-shard contracts without proxy call overhead.
---

# MultiversX Cross-Contract Storage Reads

> **Note**: This skill uses `TokenIdentifier` (ESDT-only alias). For unified EGLD+ESDT identifiers, use `TokenId`.

Read another contract's storage mappers directly — zero gas overhead from proxy calls, no async complexity.

## What Problem Does This Solve?

When your contract needs to read state from another same-shard contract, you have two options:
1. **Proxy call** — executes a view on the target, costs execution gas, requires ABI knowledge
2. **Direct storage read** — reads the raw storage key, costs only a storage read, requires knowing the key name

`storage_mapper_from_address` gives you option 2.

## When to Use

| Criteria | `storage_mapper_from_address` | Proxy Call |
|---|---|---|
| Same shard only | Yes (required) | Works cross-shard |
| Read-only | Yes | Read + Write |
| Needs computation on target | No | Yes |
| Gas cost | ~storage read cost | ~execution + storage |
| Requires knowing storage keys | Yes | No (uses ABI) |
| Data freshness | Current block state | Current block state |

## Core Pattern

```rust
#[multiversx_sc::module]
pub trait ExternalStorageModule {
    // Read a simple value from another contract
    #[storage_mapper_from_address("total_supply")]
    fn external_total_supply(
        &self,
        contract_address: ManagedAddress,
    ) -> SingleValueMapper<BigUint, ManagedAddress>;

    // Read a value with a composite key (token-specific balance)
    #[storage_mapper_from_address("balance")]
    fn external_balance(
        &self,
        contract_address: ManagedAddress,
        token_id: &TokenIdentifier,
    ) -> SingleValueMapper<BigUint, ManagedAddress>;

    // Read a boolean flag (module-prefixed key)
    #[storage_mapper_from_address("pause_module:paused")]
    fn external_paused(
        &self,
        contract_address: ManagedAddress,
    ) -> SingleValueMapper<bool, ManagedAddress>;
}
```

### Key Syntax Rules

1. The string in `#[storage_mapper_from_address("key")]` must EXACTLY match the storage key in the target contract
2. First parameter must be `ManagedAddress` — the target contract address
3. Return type's second generic must be `ManagedAddress` (e.g., `SingleValueMapper<BigUint, ManagedAddress>`)
4. Additional parameters after the address become part of the composite storage key (same as normal storage mappers with parameters)

## Generic Examples

### Example 1: Reading a Token Balance
```rust
#[storage_mapper_from_address("balance")]
fn external_token_balance(
    &self,
    contract_address: ManagedAddress,
    token_id: &TokenIdentifier,
) -> SingleValueMapper<BigUint, ManagedAddress>;

fn get_external_balance(&self, addr: &ManagedAddress, token: &TokenIdentifier) -> BigUint {
    let mapper = self.external_token_balance(addr.clone(), token);
    if mapper.is_empty() { BigUint::zero() } else { mapper.get() }
}
```

### Example 2: Reading a Config Flag
```rust
#[storage_mapper_from_address("is_active")]
fn external_is_active(
    &self,
    contract_address: ManagedAddress,
) -> SingleValueMapper<bool, ManagedAddress>;
```

### Example 3: Reading a Composite Key (Multi-Parameter)
```rust
// Target contract has: #[storage_mapper("rate")] fn rate(&self, asset: &TokenIdentifier, tier: u32) -> ...
#[storage_mapper_from_address("rate")]
fn external_rate(
    &self,
    contract_address: ManagedAddress,
    asset: &TokenIdentifier,
    tier: u32,
) -> SingleValueMapper<BigUint, ManagedAddress>;
```

## How to Discover Storage Keys

1. **Read the source code**: Look for `#[storage_mapper("key")]` in the target contract
2. **Check the ABI**: The `.abi.json` file lists all storage keys
3. **Composite keys**: Storage mappers with parameters encode the key as `key` + nested-encoded parameters
4. **Module keys**: Some modules use prefixed keys like `pause_module:paused`

## Security Considerations

### 1. Same-Shard Only
`storage_mapper_from_address` only works when both contracts are on the SAME shard. Cross-shard reads return empty/default values silently — no error!

```rust
// DANGER: If target_address is on a different shard, this returns 0 silently
let value = self.external_total_supply(target_address).get();
```

### 2. Stale Data Awareness
The data is as fresh as the current block. But if the target contract hasn't been called this block, its state reflects the last block it was active.

### 3. Storage Key Collisions
If the target contract changes its storage key names in an upgrade, your reads will break silently (return defaults).

### 4. No Write Access
This pattern is READ-ONLY. You cannot write to another contract's storage.

## Anti-Patterns

### 1. Not Validating Empty Results
```rust
// WRONG — if the key doesn't exist, you get a default (0, false, empty)
let value = self.external_balance(addr, &token).get();

// CORRECT — check if the mapper has a value
let mapper = self.external_balance(addr, &token);
require!(!mapper.is_empty(), "External value not set");
let value = mapper.get();
```

### 2. Assuming Cross-Shard Works
```rust
// WRONG — no validation
let balance = self.external_balance(unknown_address, &token).get();

// CORRECT — validate shard first
let my_shard = self.blockchain().get_shard_of_address(&self.blockchain().get_sc_address());
let target_shard = self.blockchain().get_shard_of_address(&target_address);
require!(my_shard == target_shard, "Cross-shard read not supported");
```

## Template

```rust
multiversx_sc::imports!();

#[multiversx_sc::module]
pub trait ExternalStorageModule {
    // Define one mapper per external storage key you need
    #[storage_mapper_from_address("storage_key_name")]
    fn external_value(
        &self,
        contract_address: ManagedAddress,
    ) -> SingleValueMapper<YourType, ManagedAddress>;

    // Helper to read with validation
    fn read_external_value(&self, addr: &ManagedAddress) -> YourType {
        let mapper = self.external_value(addr.clone());
        require!(!mapper.is_empty(), "External value not set");
        mapper.get()
    }
}
```

Overview

This skill exposes how to read another same-shard MultiversX contract's storage mappers directly using storage_mapper_from_address. It eliminates proxy-call overhead and async complexity by performing direct storage reads when you know the target storage key names. Use it to build aggregators, controllers, or any contract that needs fast, read-only access to another contract's state.

How this skill works

Define storage mapper signatures that take the target ManagedAddress as the first parameter and use #[storage_mapper_from_address("key")] with the exact storage key string. Calls return mapper objects tied to the target address; reading them performs a storage read (not a contract call). Additional mapper parameters become part of the composite key, and results are only valid when both contracts are in the same shard.

When to use it

  • You need low-cost, read-only access to another contract’s state
  • Both contracts are guaranteed to be on the same shard
  • You can identify the exact storage key name(s) in the target contract
  • You want to avoid proxy call gas and async flow overhead
  • You are aggregating or validating on-chain data across contracts

Best practices

  • Always match the storage key string exactly as declared in the target contract
  • Check mapper.is_empty() before using get() to detect missing keys or cross-shard returns
  • Validate both contracts are on the same shard before reading to avoid silent defaults
  • Use this pattern only for read-only requirements; it cannot write or trigger target logic
  • Prefer reading source code or the ABI to discover exact key names and parameter encodings

Example use cases

  • Read a token balance from an ESDT contract using the target address and TokenIdentifier to aggregate balances
  • Query a boolean config flag (e.g., is_active or paused) from a controller contract without invoking logic
  • Pull a composite rate or tiered parameter (asset, tier) from a pricing contract for on-chain calculations
  • Implement a multi-contract dashboard or indexer that consolidates state with minimal gas overhead
  • Validate external contract state inside access-control checks before performing local actions

FAQ

What happens if the target contract is on a different shard?

Cross-shard reads return empty/default values (e.g., 0 or false) silently — no error is thrown. Always check shard equality or mapper.is_empty().

How do I find the correct storage key names?

Read the target contract source for #[storage_mapper("key")] annotations or inspect the contract's .abi.json, which lists storage keys and parameter encodings.