# Dojo

> Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo.

- Skill: `cartridge-gg/dojo` (Agent Skill)
- Install (CLI): `npx skillmds@latest add cartridge-gg/dojo`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cartridge-gg/dojo/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: cartridge-gg (https://skillmd.com/u/cartridge-gg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cartridge-gg/dojo

---


# Dojo Engine Framework

This skill covers Dojo Engine patterns for building fully on-chain games on Starknet. For pure Cairo language and Starknet contract patterns, see the `cairo` skill.

## When to Use

- Creating or modifying Dojo contracts (`#[dojo::contract]`)
- Defining models (`#[dojo::model]`) or events (`#[dojo::event]`)
- Working with WorldStorage (read/write models, emit events)
- Composing Starknet components within Dojo contracts
- Testing with `spawn_test_world`
- Configuring permissions and deployment

## Architecture Overview

Dojo is an Entity Component System (ECS) framework:

- **World** — Central coordinator and database, manages all resources within namespaces
- **Models (What)** — Data structs stored in the World (`#[dojo::model]`)
- **Systems (How)** — Stateless contracts that operate on models (`#[dojo::contract]`)
- **Events** — Indexed signals for off-chain sync (`#[dojo::event]`)
- **Components** — Reusable logic modules (`#[starknet::component]`)

### Data Flow

```
System (#[dojo::contract])
  └── self.world(@NAMESPACE()) → WorldStorage
       ├── world.read_model(key) → Model
       ├── world.write_model(@model)
       └── world.emit_event(@event)
```

## Essential Imports

```cairo
// In systems (always needed)
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
use dojo::world::WorldStorage;

// In models
use starknet::ContractAddress;

// For nested structs in models
use dojo::meta::Introspect;

// In tests
use dojo_cairo_test::{
    ContractDef, ContractDefTrait, NamespaceDef, TestResource,
    WorldStorageTestTrait, spawn_test_world,
};
```

## Models (`#[dojo::model]`)

Models are the state. Structs annotated with `#[dojo::model]` are stored in the World.

```cairo
#[derive(Drop, Serde)]
#[dojo::model]
pub struct Game {
    #[key]
    pub id: u64,           // Primary key — used for lookup
    pub level: u8,         // Data fields
    pub score: u32,
    pub slots: felt252,    // Can store packed data
}
```

### Key Rules

1. **At least one `#[key]` field required**
2. **Keys must come first** in the struct
3. **Keys are not stored** — used only for indexing
4. **Required derives**: `Drop`, `Serde`
5. **Optional derives**: `Copy` (for primitive types)

### Composite Keys

```cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct VaultPosition {
    #[key]
    pub user: felt252,       // First key
    pub shares: u256,
    pub lockup: u64,
}

// Read with single key
let position: VaultPosition = world.read_model(user_felt);

// For multiple keys, use tuple
let resource: GameResource = world.read_model((player, location));
```

### Custom Nested Structs

Must derive `Introspect`:

```cairo
#[derive(Drop, Copy, Serde, Introspect)]
pub struct Vec2 {
    pub x: u32,
    pub y: u32,
}
```

### Model API

```cairo
let mut world = self.world(@"NUMS");

// Read (returns default/zero if not set)
let game: Game = world.read_model(game_id);

// Write / Update
world.write_model(@game);

// Generate unique ID
let entity_id = world.uuid();

// Delete
world.erase_model(@game);
```

## Events (`#[dojo::event]`)

Events are automatically indexed by Torii (Dojo's off-chain indexer).

```cairo
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct Purchased {
    #[key]
    pub player_id: felt252,    // Indexed field for filtering
    pub starterpack_id: u32,
    pub quantity: u32,
    pub time: u64,
}

// Emit in a system or component
world.emit_event(@Purchased {
    player_id: caller.into(),
    starterpack_id: 1,
    quantity: 1,
    time: get_block_timestamp(),
});
```

## Systems (`#[dojo::contract]`)

Systems are **thin entry points**. They get WorldStorage and delegate to components.

```cairo
#[starknet::interface]
pub trait IPlay<T> {
    fn set(ref self: T, game_id: u64, index: u8) -> u16;
    fn select(ref self: T, game_id: u64, index: u8);
}

#[dojo::contract]
pub mod Play {
    use dojo::model::ModelStorage;
    use crate::components::playable::PlayableComponent;
    use crate::constants::NAMESPACE;

    // Embed components
    component!(path: PlayableComponent, storage: playable, event: PlayableEvent);
    impl PlayableInternalImpl = PlayableComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        playable: PlayableComponent::Storage,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        PlayableEvent: PlayableComponent::Event,
    }

    #[abi(embed_v0)]
    impl PlayImpl of super::IPlay<ContractState> {
        fn set(ref self: ContractState, game_id: u64, index: u8) -> u16 {
            // Get world for namespace
            let world = self.world(@NAMESPACE());
            // Delegate to component
            self.playable.set(world, game_id, index)
        }
    }
}
```

### World Access

```cairo
// With explicit namespace (preferred in this project)
let world = self.world(@NAMESPACE());  // NAMESPACE() returns "NUMS"

// With default namespace
let world = self.world_default();

// With inline namespace
let world = self.world(@"my_namespace");
```

### Initialization (`dojo_init`)

Called once on contract deployment:

```cairo
fn dojo_init(ref self: ContractState, owner: ContractAddress, entry_price: u128) {
    let mut world = self.world(@NAMESPACE());
    let mut store = StoreImpl::new(world);

    // Create initial config
    let config = ConfigTrait::new(owner, entry_price);
    store.set_config(config);

    // Initialize components
    self.initializable.initialize(world);
}
```

## Component Pattern (Scoundrel-specific)

Components contain the **business logic** and receive `WorldStorage` as a parameter.

```cairo
#[starknet::component]
pub mod PlayableComponent {
    use dojo::world::WorldStorage;
    use crate::{StoreImpl, StoreTrait};

    #[storage]
    pub struct Storage {}

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {}

    #[generate_trait]
    pub impl InternalImpl<
        TContractState,
        +HasComponent<TContractState>,
        +Drop<TContractState>,
    > of InternalTrait<TContractState> {
        fn set(
            ref self: ComponentState<TContractState>,
            world: WorldStorage,
            game_id: u64,
            index: u8,
        ) -> u16 {
            // [Setup] Store
            let mut store = StoreImpl::new(world);

            // [Check] Game state
            let caller = starknet::get_caller_address();
            let mut game = store.game(game_id);
            game.assert_does_exist();
            game.assert_not_over();

            // [Effect] Place number
            game.place(game.number, index, ref rand);

            // [Interaction] Write back
            store.set_game(@game);
            game.number
        }
    }
}
```

### Dependent Component Access

```cairo
// Immutable access
let questable = get_dep_component!(@self, Quest);
questable.progress(world, player, task_id, count, true);

// Mutable access
let mut rankable = get_dep_component_mut!(ref self, Rankable);
rankable.submit(world: world, leaderboard_id: 1, score: 42);
```

### Component Embedding in Contract

```cairo
// 1. Declare component
component!(path: PlayableComponent, storage: playable, event: PlayableEvent);
impl PlayableInternalImpl = PlayableComponent::InternalImpl<ContractState>;

// 2. Add to storage
#[storage]
struct Storage {
    #[substorage(v0)]
    playable: PlayableComponent::Storage,
}

// 3. Add to events
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
    #[flat]
    PlayableEvent: PlayableComponent::Event,
}
```

## OpenZeppelin + Dojo Integration

OZ components compose with Dojo contracts using the same `component!()` pattern:

```cairo
#[dojo::contract]
pub mod Vault {
    use openzeppelin::token::erc20::extensions::erc4626::ERC4626Component;
    use openzeppelin::access::accesscontrol::AccessControlComponent;

    component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event);
    component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);

    // ERC4626 hooks for custom deposit/withdraw logic
    impl ERC4626HooksImpl of ERC4626Component::ERC4626HooksTrait<ContractState> {
        fn before_deposit(ref self: ERC4626Component::ComponentState<ContractState>, ...) {
            let mut contract_state = self.get_contract_mut();
            contract_state.pausable.assert_not_paused();
            let world = contract_state.world(@NAMESPACE());
            // ... custom logic using world
        }
    }
}
```

