# Dojo Enums and Uninitialized Storage We have identified an issue in Dojo storage when working with enums and uninitialized storage. ## Dojo Storage Overview Before describing the issue, here's a brief summary of how Dojo storage works: 1. A model is defined as a Cairo struct. 2. This model is serialized using the `Serde` trait and written to world storage via `world.model_write(@m)`. 3. The world contract's storage acts as a database, where serialized data is written through syscalls to specific storage locations. Since serialization is handled by the `Serde` trait, enums are serialized as follows: 1. The variant index is stored as the first `felt`. 2. If the variant contains a value (i.e., is not the unit type `()`), the serialized value occupies the remaining `felt`s. For example, the `Option<T>` enum is serialized as: ```rust enum Option<T> { Some: T, None, } let a = Option::None; // Serialized = [0x1] let b = Option::Some(2); // Serialized = [0x0, 0x2] ``` In Starknet contracts, the Cairo compiler increments variant indices by `1`, ensuring that uninitialized storage defaults to a predictable variant. Since Dojo uses `Serde`, this increment is not happening. ## Security Considerations Given this behavior, consider the following model: ```rust struct MyModel { #[key] id: u32, score: Option<u32>, } ``` If this model is read from storage before being explicitly written, the world’s storage remains uninitialized (filled with `0x0`s). This results in: ```rust let my_key: u32 = 0x1234; // Reading an uninitialized model from storage. let m = world.read_model(my_key); // This assertion will revert Cairo execution. // assert(m == None) if m.score.is_some() { // Unexpected execution: `Some(0)` is returned instead of `None`. } else { // Expected behavior, but will not occur in this case. } ``` Here, `Some(0)` is returned instead of `None` because `Some` is the first variant of `Option<T>`, leading to unintended behavior when relying on `score` for logic for uninitialized models. For custom enums, consider: ```rust enum MyEnum { NotSet, Submitted: u32, Validated: felt252, } ``` Reading uninitialized storage will return `MyEnum::NotSet`, as it is the first variant. ## Solutions at application level 1. Handling `Option<T>` at the Application Level - Ensure models are explicitly initialized before being used. - Avoid relying on `Option<T>` for initialization checks. Instead, use a separate `bool` or `integer` field, as these default to `0x0`. 2. Ordering Custom Enum Variants - Define the default variant as the first variant to ensure correct behavior when reading uninitialized storage. ## Considerations for Dojo Core fix We are investigating solutions that do not break existing storage in deployed worlds, though this presents challenges. Possible Approaches: 1. **Introducing `DojoOption<T>`** - A custom `DojoOption<T>` type where `None` is explicitly stored as `0x0`. - Implementing `Into<Option<T>>` would allow seamless conversion. - This approach is non-breaking but requires model upgrades or conversions while fetching data. 2. **Incrementing Variant Indices** - Apply the Cairo compiler’s approach by shifting variant indices by `1`. - `0x0` would then explicitly map to the default variant. - This would break existing storage and lacks backward compatibility + some challenges for the impl. 3. **Modifying `Introspect` for `Option<T>`** - Adjust `Option<T>` handling internally to map `None` to `0x0` and `Some` to `0x1`. - Like the previous approach, this would break existing storage. ## Conclusion If your project relies on `Option<T>` or custom enums, this issue may be critical. We recommend reviewing your usage and considering explicit initialization strategies when applicable. For projects already on `mainnet`, upgrading the contract to modify logic or adding a dedicated initialization field can mitigate potential security risks. This issue affects all versions since Dojo `1.0.0`.