How to Use DynArray Storage in GenLayer Contracts: A Complete Guide

DynArray is the deterministic, dynamic-array storage type used in GenLayer contracts to provide O(1) appends and persistent index-based access for data verified by the GenVM.

GenLayer contracts rely on specialized collection types for deterministic on-chain storage that survives across transactions. The DynArray type, available in the genlayerlabs/genlayer-project-boilerplate repository, acts like a Python list but is fully compatible with the GenVM linter and runtime. Learning how to use DynArray storage in GenLayer contracts ensures your state remains valid, persistent, and accepted by the GenVM.

Why Choose DynArray for GenLayer Contract Storage

The GenVM tracks every element stored in a DynArray and automatically persists the array’s length in the contract’s state. This guarantees that data remains available across transactions without manual serialization.

Key characteristics of DynArray storage include:

  • O(1) append and O(1) access by index, making it efficient for sequential data.
  • Automatic length tracking by the VM, which updates the stored metadata on every mutation.
  • Deterministic behavior required for consensus, unlike native Python collections.

As documented in the repository, native Python types such as list and dict are rejected by the linter. The README.md explicitly notes that invalid storage types must be replaced with approved types such as TreeMap, DynArray, or u256 (see README.md#L64-L66). Similarly, CLAUDE.md lists the supported storage types and explains the linter’s restrictions (CLAUDE.md#L72-L73).

Declaring a DynArray with Storage-Compatible Types

To store custom objects inside a DynArray, you must decorate the class with @allow_storage and @dataclass. The generic syntax DynArray[<type>] declares the element type for the compiler.

from genlayer import *

@allow_storage
@dataclass
class Item:
    id: u256
    name: str

class MyContract(gl.Contract):
    items: DynArray[Item]          # Declaration of a dynamic array of `Item`

The type parameter inside DynArray[...] must be a storage-compatible class. Built-in types such as u256 are also valid. Without @allow_storage, the GenVM linter will refuse to store the custom class in contract state.

Appending and Reading Elements

New elements are added with append(), which executes in constant time. Reading by index uses standard bracket notation, and the VM performs automatic bounds checking that reverts the transaction on an out-of-range access.

@gl.public.write
def add_item(self, item_id: u256, item_name: str) -> None:
    new_item = Item(id=item_id, name=item_name)
    self.items.append(new_item)    # O(1) push

@gl.public.view
def get_item(self, index: u256) -> Item:
    # Bounds checking is automatic; out‑of‑range raises a revert

    return self.items[index]

Because the VM does not perform implicit type coercion, you should validate inputs before appending them to the array.

Iterating Over and Removing Elements

DynArray implements the Python iterator protocol, so you can safely loop over it inside view methods. The type also supports pop() to remove the last element and remove_at(index) to delete a specific index and shift subsequent elements left. Both operations update the stored length automatically.

@gl.public.view
def list_names(self) -> list[str]:
    return [item.name for item in self.items]

@gl.public.write
def pop_last(self) -> None:
    self.items.pop()               # Removes the final element

When you need keyed lookups rather than ordered traversal, the repository’s example contracts recommend using TreeMap instead. The contracts/football_bets.py file demonstrates indexed storage patterns (contracts/football_bets.py#L22-L25) that apply equally to DynArray when choosing the right collection for your data layout.

Complete DynArray Contract Example

The following ScoreBoard contract, modeled after contracts/dynarray_example.py, shows the full lifecycle of a DynArray: declaration, append, length inspection, index lookup, and removal.


# contracts/dynarray_example.py

from genlayer import *

@allow_storage
@dataclass
class Score:
    player: Address
    points: u256

class ScoreBoard(gl.Contract):
    # A dynamic array that stores Score objects

    scores: DynArray[Score]

    @gl.public.write
    def record_score(self, player: str, pts: u256) -> None:
        addr = Address(player)
        self.scores.append(Score(player=addr, points=pts))

    @gl.public.view
    def total_players(self) -> u256:
        return self.scores.length          # length property is always available

    @gl.public.view
    def get_score(self, idx: u256) -> Score:
        return self.scores[idx]

    @gl.public.write
    def remove_last(self) -> None:
        self.scores.pop()

This example follows the repository’s pattern of separating write methods (record_score, remove_last) from view methods (total_players, get_score) to keep state mutations explicit.

Testing DynArray Storage Operations

You can exercise the contract in direct test mode by deploying the file and calling its methods through the test fixtures provided in the boilerplate.

def test_dynarray(direct_vm, direct_deploy, direct_alice):
    contract = direct_deploy("contracts/dynarray_example.py")
    direct_vm.sender = direct_alice

    contract.record_score("0x1111111111111111111111111111111111111111", 10)
    contract.record_score("0x2222222222222222222222222222222222222222", 20)

    assert contract.total_players() == 2
    assert contract.get_score(0).points == 10
    contract.remove_last()
    assert contract.total_players() == 1

The test confirms that append() increases the length, bracket indexing returns the correct element, and pop() decrements the stored count.

Storage Best Practices for GenLayer Contracts

Follow these rules to keep your DynArray usage deterministic and linter-compliant:

  • Never store native Python collections such as list or dict. The GenVM linter rejects them at compile time, as enforced by the rules in CLAUDE.md and README.md.
  • Always apply @allow_storage to any custom class you intend to persist inside a DynArray.
  • Validate all inputs before appending. The VM does not coerce types automatically.
  • Prefer TreeMap for keyed lookups and reserve DynArray for ordered, index-based data, aligning with the design shown in contracts/football_bets.py.

Summary

  • DynArray is the GenVM-approved dynamic array for persistent, deterministic contract storage.
  • Declare it with DynArray[<type>] and decorate custom classes using @allow_storage.
  • Use append() for O(1) inserts, bracket indexing for reads, and pop() or remove_at() for deletions.
  • Native Python collections are forbidden; consult CLAUDE.md and README.md for the approved type list.
  • Write explicit tests using the boilerplate’s direct-mode fixtures to verify length and element access.

Frequently Asked Questions

What happens if I use a native Python list instead of DynArray in a GenLayer contract?

The GenVM linter rejects native Python collections at compile time. According to the storage rules documented in CLAUDE.md#L72-L73 and README.md#L64-L66, you must replace list and dict with approved types such as DynArray or TreeMap.

Does DynArray support arbitrary Python types?

No. The generic parameter in DynArray[<type>] must be a storage-compatible type, such as u256 or a custom class decorated with @allow_storage and @dataclass. Arbitrary Python objects that lack these annotations cannot be persisted in contract state.

How do I check the number of elements in a DynArray?

Access the length property on the array instance. The VM updates this value automatically on every append(), pop(), or remove_at() operation, so it always reflects the current stored element count.

Is DynArray or TreeMap better for keyed lookups in GenLayer?

Use TreeMap when you need keyed or associative lookups. Reserve DynArray for ordered, index-based data, as demonstrated by the storage patterns in contracts/football_bets.py.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →