Common `@allow_storage` Decorator Pitfalls in GenLayer Contracts
The most common pitfall with the @allow_storage decorator is placing it after @dataclass, which causes the GenLayer compiler to ignore it entirely and reject the class as an invalid storage type.
The @allow_storage decorator enables custom Python classes to persist in GenLayer contract storage, but it imposes strict constraints that differ from standard Python patterns. As implemented in the genlayerlabs/genlayer-project-boilerplate repository, these constraints ensure deterministic state serialization—a core requirement for blockchain execution. This guide examines the nine most frequent developer errors, with concrete examples from the FootballBets contract in contracts/football_bets.py and authoritative references from CLAUDE.md.
Decorator Order: The Silent Failure
The GenLayer compiler only recognizes @allow_storage when it appears before any other class decorator. When placed after @dataclass, the class is treated as a plain Python object, triggering linting errors about invalid storage types.
Incorrect:
@dataclass
@allow_storage # ❌ Ignored by compiler
class WrongOrder:
value: u256
Correct (as in contracts/football_bets.py):
@allow_storage # ✅ Must be outermost
@dataclass
class Bet:
id: str
has_resolved: bool
game_date: str
resolution_url: str
team1: str
team2: str
predicted_winner: str
real_winner: str
real_score: str
The Bet class in contracts/football_bets.py demonstrates this pattern correctly—the decorator stack order ensures the compiler registers the class for storage serialization.
Using Unsupported Native Containers
GenLayer storage cannot hold native Python list or dict objects because their memory layout is nondeterministic across executions. The approved container types are:
TreeMap[K, V]— ordered key-value mappingDynArray[T]— dynamically sized arrayArray[T, N]— fixed-size array- Primitive numeric types:
u256,i256
Incorrect:
@allow_storage
@dataclass
class BadBet:
scores: list = [] # ❌ Native list rejected
metadata: dict = {} # ❌ Native dict rejected
Correct:
@allow_storage
@dataclass
class FixedBet:
scores: DynArray[str] = DynArray() # ✅ GenLayer container
metadata: TreeMap[str, str] = TreeMap() # ✅ GenLayer mapping
Mutable Default Values and Shared State
Even with approved container types, using field(default_factory=list) or similar patterns creates a shared mutable instance across all contract objects. This violates GenLayer's deterministic state principle.
Problematic:
from dataclasses import field
@allow_storage
@dataclass
class RiskyBet:
entries: DynArray[str] = field(default_factory=DynArray) # ❌ Lint error
Resolution:
Initialize containers explicitly within contract methods rather than as field defaults, or use the direct instantiation pattern shown above with DynArray() when the type itself is allowed.
Embedding Nondeterministic Objects
Storing fields that depend on gl.nondet.* functions—such as web calls (gl.nondet.get_webpage) or LLM prompts (gl.nondet.prompt)—makes persisted state nondeterministic. This breaks GenLayer's equivalence-principle checks, where multiple validators must reach identical conclusions.
Keep all nondeterministic logic strictly within contract methods, never as stored field values.
Recursive and Circular References
A class that references itself directly or indirectly causes infinite serialization loops. Common patterns that fail:
TreeMap[Address, MyClass]whereMyClasscontains anotherTreeMapwith the same class- Self-referencing linked list structures
Avoid:
@allow_storage
@dataclass
class Node: # ❌ Self-reference breaks compiler
value: u256
next_node: Optional['Node']
Prefer:
Store identifiers and use separate mapping tables:
@allow_storage
@dataclass
class FlatNode:
node_id: u256
value: u256
next_id: Optional[u256] # ✅ Reference by ID
# In contract: TreeMap[u256, FlatNode] for lookup
Missing Type Annotations
GenLayer's compiler generates storage schemas from static type information. Omitting annotations or using Any prevents schema construction.
Insufficient:
@allow_storage
@dataclass
class VagueData:
unlabeled = 0 # ❌ No annotation
anything: Any # ❌ Ambiguous type
Required:
@allow_storage
@dataclass
class PreciseData:
counter: u256 # ✅ Concrete type
owner: Address # ✅ GenLayer primitive
labels: TreeMap[str, DynArray[str]] # ✅ Nested approved types
Inheritance Limitations
Subclassing an @allow_storage class is not supported and breaks serialization. The flattening and composition approach remains the only viable pattern.
Do not use:
@allow_storage
@dataclass
class BaseRecord:
id: u256
@allow_storage # ❌ Inheritance unsupported
@dataclass
class ExtendedRecord(BaseRecord):
extra: str
Instead compose:
@allow_storage
@dataclass
class CoreFields:
id: u256
@allow_storage
@dataclass
class FullRecord:
core: CoreFields # ✅ Composition
extra: str
Functions as Field Values
Callable objects, including functions and lambdas, cannot be serialized into blockchain state. Fields must contain only data.
Invalid:
@allow_storage
@dataclass
class InvalidLogic:
validator: Callable[[str], bool] = some_func # ❌ Cannot serialize
Unbounded Data Structure Size
Extremely large DynArray or TreeMap instances risk exceeding gas limits or causing performance bottlenecks during access operations. Design with explicit bounds or shard data across multiple mappings.
Summary
- Apply
@allow_storagebefore@dataclassor any other decorator - Use only GenLayer-approved container types:
TreeMap,DynArray,Array - Avoid mutable defaults and nondeterministic objects in stored fields
- Prevent circular references by using identifier-based lookups
- Provide explicit type annotations for every field
- Do not use inheritance with storage-enabled classes
- Store data only, never functions or callables
- Design bounded data structures to respect gas limits
Frequently Asked Questions
Why does decorator order matter with @allow_storage?
The GenLayer compiler processes decorators from bottom to top. When @allow_storage appears after @dataclass, the dataclass transformation completes before the storage registration can intercept it, causing the compiler to miss the annotation entirely. This results in the class being rejected as an invalid storage type during linting.
Can I use Python's standard list and dict in @allow_storage classes?
No. Native list and dict objects have nondeterministic memory representations that vary across Python implementations and executions. GenLayer requires deterministic serialization for consensus. Replace list with DynArray or Array, and dict with TreeMap—all defined in the GenLayer standard library.
How do I handle optional or default values in storage classes?
Use the standard Optional[T] annotation for nullable fields. For defaults, avoid field(default_factory=...) with mutable types. Either initialize values explicitly in your contract methods, or use direct instantiation like DynArray() when the container type itself is storage-approved.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →