# How Sway Storage Allocation and Storage Keys Work: Compile-Time Key Generation in Fuel VM Contracts

> Understand how Sway storage allocation and storage keys work. Learn about compile-time key generation in Fuel VM contracts for organized data management.

- Repository: [Fuel Labs/sway](https://github.com/FuelLabs/sway)
- Tags: internals
- Published: 2026-03-04

---

**Sway computes deterministic 32-byte storage keys at compile time by hashing human-readable paths that encode namespace hierarchies and field names, then represents these keys in the compiler IR to generate Fuel VM storage instructions.**

In the FuelLabs/sway repository, contract data persistence relies on a sophisticated compile-time key generation system that maps logical storage declarations to specific slots in the Fuel VM's persistent key-value store. Understanding how Sway storage allocation and storage keys work is essential for developers optimizing contract storage layout and predicting state access patterns.

## How Sway Generates Storage Keys

The compiler derives storage keys through a deterministic three-step pipeline that transforms human-readable declarations into cryptographically secure 32-byte identifiers. This process occurs in [`sway-core/src/ir_generation/storage.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/ir_generation/storage.rs) and [`sway-ir/src/storage_key.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/storage_key.rs).

### Step 1: Constructing the Storage Path

The process begins with `get_storage_key_string`, which constructs a canonical string encoding the top-level `storage` namespace, any nested namespaces, and the variable name. For top-level variables, the function concatenates `STORAGE_TOP_LEVEL_NAMESPACE`, `STORAGE_FIELD_SEPARATOR`, and the identifier. For nested namespaces, it inserts `STORAGE_NAMESPACE_SEPARATOR` between hierarchical components:

```rust
// sway-core/src/ir_generation/storage.rs
pub fn get_storage_key_string(storage_field_names: &[String]) -> String {
    if storage_field_names.len() == 1 {
        format!(
            "{}{}{}",
            sway_utils::constants::STORAGE_TOP_LEVEL_NAMESPACE,
            sway_utils::constants::STORAGE_FIELD_SEPARATOR,
            storage_field_names.last().unwrap(),
        )
    } else {
        format!(
            "{}{}{}{}{}",
            sway_utils::constants::STORAGE_TOP_LEVEL_NAMESPACE,
            sway_utils::constants::STORAGE_NAMESPACE_SEPARATOR,
            storage_field_names
                .iter()
                .take(storage_field_names.len() - 1)
                .cloned()
                .collect::<Vec<_>>()
                .join(sway_utils::constants::STORAGE_NAMESPACE_SEPARATOR),
            sway_utils::constants::STORAGE_FIELD_SEPARATOR,
            storage_field_names.last().unwrap(),
        )
    }
}

```

### Step 2: Hashing with Domain Separation

The `hash_storage_key_string` function prefixes the path with a domain byte (`0u8`) to separate compiler-generated keys from user-supplied keys (such as those used by `StorageMap`), then applies SHA-256 hashing:

```rust
// sway-core/src/ir_generation/storage.rs
fn hash_storage_key_string(storage_key_string: &str) -> Bytes32 {
    let mut hasher = Hasher::default();
    hasher.input(sway_utils::constants::STORAGE_DOMAIN);
    hasher.input(storage_key_string);
    hasher.finalize()
}

```

The public helper `get_storage_key` determines whether to use this hashed value or an explicit user-provided key:

```rust
// sway-core/src/ir_generation/storage.rs
pub(super) fn get_storage_key(storage_field_names: Vec<String>, key: Option<U256>) -> Bytes32 {
    match key {
        Some(key) => key.to_be_bytes().into(),
        None => hash_storage_key_string(&get_storage_key_string(&storage_field_names)),
    }
}

```

### Step 3: IR Representation as StorageKey

In [`sway-ir/src/storage_key.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/storage_key.rs), the compiler creates a `StorageKey` value within the IR's `Context`. This struct wraps a constant of type `{ b256, u64, b256 }` containing the slot hash, word offset within the slot, and an optional field identifier for struct members:

```rust
// sway-ir/src/storage_key.rs
pub struct StorageKey(#[in_context(storage_keys)] pub slotmap::DefaultKey);

impl StorageKey {
    pub fn new(context: &mut Context, slot: [u8; 32], offset: u64, field_id: [u8; 32]) -> Self {
        let b256_ty = Type::get_b256(context);
        let uint64_ty = Type::get_uint64(context);
        let key_ty = Type::new_struct(context, vec![b256_ty, uint64_ty, b256_ty]);
        let ptr_ty = Type::new_typed_pointer(context, key_ty);

        let slot   = ConstantContent::new_b256(context, slot);
        let offset = ConstantContent::new_uint(context, 64, offset);
        let field_id = ConstantContent::new_b256(context, field_id);

        let key = ConstantContent::new_struct(
            context,
            vec![b256_ty, uint64_ty, b256_ty],
            vec![slot, offset, field_id],
        );
        let key = Constant::unique(context, key);
        StorageKey(context.storage_keys.insert(StorageKeyContent { ptr_ty, key }))
    }
}

```

## Storage Allocation in the Compiler IR

Once keys are generated, the compiler performs allocation steps to map logical storage accesses to Fuel VM instructions, primarily in [`sway-core/src/ir_generation/function.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/ir_generation/function.rs).

### Determining Slot and Offset

For top-level variables, the slot equals the hashed key directly. When accessing struct fields, the compiler calculates word offsets by dividing byte offsets by 8 (the Fuel VM word size), then adds this to the base slot using `add_to_b256`. The `field_id` parameter in `StorageKey::new` contains the hash of the struct-field path for member access, or zero for top-level variables.

### Emitting Storage Instructions

The compiler creates a `StorageKey` instance via `StorageKey::new(context, slot, offset, field_id)` and emits a `GetStorageKey` IR instruction. This instruction is later lowered to Fuel VM `state_read_*` or `state_write_*` intrinsics. For constant initialization, `serialize_to_storage_slots` in [`sway-core/src/ir_generation/storage.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/ir_generation/storage.rs) walks constant values, computes final keys via `add_to_b256` for each word, and emits `StorageSlot` structures consumed by the VM.

## Practical Example: Computing Storage Keys

Consider this Sway contract demonstrating namespace hierarchies and the generated paths:

```sway
// storage_decls.sw
storage {
    // Top-level variable `counter` → key = hash("storage.counter")
    counter: u64 = 0,
    // Nested namespace `my_ns::value`
    // → key = hash("storage::my_ns.value")
    my_ns {
        value: u8 = 42
    }
}

```

The compiler generates these keys deterministically using the internal helpers:

```rust
use sway_core::ir_generation::storage::{get_storage_key_string, get_storage_key};

fn main() {
    // 1️⃣ Counter (top-level)
    let counter_path = vec!["counter".into()];
    let counter_key = get_storage_key(counter_path.clone(), None);
    println!("counter key: 0x{:x}", Bytes32::from(counter_key));

    // 2️⃣ my_ns.value (namespaced)
    let ns_path = vec!["my_ns".into(), "value".into()];
    let ns_key = get_storage_key(ns_path.clone(), None);
    println!("my_ns.value key: 0x{:x}", Bytes32::from(ns_key));

    // 3️⃣ Offset example – field `inner` inside a struct stored at `my_struct`
    // Suppose `my_struct` occupies slot S, and `inner` is 8 bytes after the start.
    // Offset in words = 8 / 8 = 1
    let base_key = get_storage_key(vec!["my_struct".into()], None);
    let offset_key = add_to_b256(base_key, 1);
    println!("my_struct.inner key (slot+1): 0x{:x}", Bytes32::from(offset_key));
}

```

## Summary

- **Deterministic hashing** ensures identical storage declarations always produce identical 32-byte keys across compilations, guaranteeing reproducible contract deployments.
- **Domain separation** via a leading `0u8` byte prevents collisions between compiler-generated keys and user-supplied map keys in `StorageMap`.
- **Path encoding** in `get_storage_key_string` combines namespaces, variable names, and field names into human-readable strings before SHA-256 hashing.
- **IR representation** uses the `StorageKey` struct in [`sway-ir/src/storage_key.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/storage_key.rs) to carry both type information and constant values, enabling storage-aware optimizations.
- **Offset calculation** addresses struct fields by adding word-based offsets to base slots using `add_to_b256` in [`sway-core/src/ir_generation/function.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/ir_generation/function.rs).

## Frequently Asked Questions

### How does Sway prevent storage key collisions between compiler-generated and user-supplied keys?

The compiler implements **domain separation** by prefixing storage path strings with a domain byte (`0u8`) in `hash_storage_key_string` before hashing. This distinguishes compiler-generated keys (used for static storage variables) from user-supplied keys (used by dynamic collections like `StorageMap`), ensuring they occupy distinct key spaces even if path strings coincide.

### Can developers predict their contract's storage keys before deployment?

Yes. Since Sway uses deterministic SHA-256 hashing of canonical path strings in [`sway-core/src/ir_generation/storage.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/ir_generation/storage.rs), developers can compute expected keys by replicating the `get_storage_key_string` and `hash_storage_key_string` logic. The same source code always generates the same 32-byte identifiers, allowing offline key prediction for integration testing.

### How does Sway calculate storage slots for struct fields?

The compiler calculates byte offsets based on field layout within the struct, divides by 8 to convert to **word offsets** (the Fuel VM's native addressing unit), and adds these to the base slot using `add_to_b256` in [`sway-core/src/ir_generation/function.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/ir_generation/function.rs). This allows efficient packing of multiple fields within the 32-byte storage slot granularity.

### What is the relationship between the IR StorageKey and the final VM storage operations?

The **IR `StorageKey`** (defined in [`sway-ir/src/storage_key.rs`](https://github.com/FuelLabs/sway/blob/main/sway-ir/src/storage_key.rs)) is a compile-time constant struct containing the slot hash, word offset, and field identifier. During code generation, the compiler translates `GetStorageKey` instructions containing these values into Fuel VM `state_read_*` and `state_write_*` intrinsics that operate on the raw 32-byte storage addresses.