dojo

star 0

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

cartridge-gg By cartridge-gg schedule Updated 4/3/2026

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

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

// 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.

#[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

#[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:

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

Model API

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).

#[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.

#[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

// 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:

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.

#[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

// 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

// 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:

#[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
        }
    }
}
Install via CLI
npx skills add https://github.com/cartridge-gg/scoundrel --skill dojo
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
cartridge-gg
cartridge-gg Explore all skills →