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
- At least one
#[key]field required - Keys must come first in the struct
- Keys are not stored — used only for indexing
- Required derives:
Drop,Serde - 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
}
}
}