Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ jobs:
module: registry
runner: ubuntu-latest-8
fuzzer_runs: 32
- package: game_components_embeddable_game_standard
module: token_lite
runner: ubuntu-latest-8
fuzzer_runs: 32
# Metagame
- package: game_components_metagame
module: leaderboard
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ jobs:
add game_components_embeddable_game_standard minigame ubuntu-latest-8 32
add game_components_embeddable_game_standard metagame ubuntu-latest-8 32
add game_components_embeddable_game_standard registry ubuntu-latest-8 32
add game_components_embeddable_game_standard token_lite ubuntu-latest-8 32
fi
if [ "$NEED_METAGAME" = "true" ]; then
add game_components_metagame leaderboard ubuntu-latest-4 256
Expand Down
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,22 @@ When adding a new module to a group package, update **both** files:
after_n_builds: 15 # ← Must equal total module count in matrix
```

### Current Matrix (16 modules)
### Current Matrix (18 modules)

| Group Package | Module | Runner | Fuzzer Runs |
|---------------|--------|--------|-------------|
| `embeddable_game_standard` | `token` | `ubuntu-latest-32` | 32 |
| `embeddable_game_standard` | `minigame` | `ubuntu-latest-32` | 32 |
| `embeddable_game_standard` | `metagame` | `ubuntu-latest-32` | 256 |
| `embeddable_game_standard` | `registry` | `ubuntu-latest-32` | 256 |
| `embeddable_game_standard` | `minigame` | `ubuntu-latest-8` | 32 |
| `embeddable_game_standard` | `metagame` | `ubuntu-latest-8` | 32 |
| `embeddable_game_standard` | `registry` | `ubuntu-latest-8` | 32 |
| `embeddable_game_standard` | `token_lite` | `ubuntu-latest-8` | 32 |
| `metagame` | `leaderboard` | `ubuntu-latest-4` | 256 |
| `metagame` | `registration` | `ubuntu-latest-4` | 256 |
| `metagame` | `entry_requirement` | `ubuntu-latest-4` | 256 |
| `metagame` | `entry_fee` | `ubuntu-latest-4` | 256 |
| `metagame` | `prize` | `ubuntu-latest-4` | 256 |
| `metagame` | `ticket_booth` | `ubuntu-latest-4` | 256 |
| `metagame` | `merkledrop` | `ubuntu-latest-4` | 256 |
| `economy` | `tokenomics` | `ubuntu-latest-4` | 256 |
| `utilities` | `math` | `ubuntu-latest-4` | 256 |
| `utilities` | `distribution` | `ubuntu-latest-4` | 256 |
Expand Down
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ codecov:
notify:
# Must equal package count in .github/workflows/main-ci.yml matrix
# See AGENTS.md "CI Configuration" section when adding packages
after_n_builds: 17
after_n_builds: 18

comment:
layout: "diff, files, header, footer"
Expand Down
1 change: 1 addition & 0 deletions packages/embeddable_game_standard/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod metagame;
pub mod minigame;
pub mod registry;
pub mod token;
pub mod token_lite;
5 changes: 5 additions & 0 deletions packages/embeddable_game_standard/src/token_lite.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod interface;

#[cfg(test)]
mod tests;
pub mod token_lite_component;
54 changes: 54 additions & 0 deletions packages/embeddable_game_standard/src/token_lite/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Token Lite Module — CoreTokenLiteComponent (ERC721)

Gas-optimized single-game variant of the `token` module ("denshokan lite").
Built for deployments that embed exactly one game, never used the multi-game
registry/objectives/context/skills/per-token renderer features, and keep
game-over / objective-completion authority in the game contract itself.

## Design Rules

| Rule | Consequence |
| --- | --- |
| One game, configured at init | No registry, no `game_id` resolution, no SRC5 probes on mint |
| No mutable token state | No `update_game`, no metagame callbacks; `is_playable` = lifecycle window only, zero storage reads |
| Token id layout is canonical | Reuses `token::structs::pack_token_id` (251-bit) bit-for-bit; unused fields (`game_id`, `objective_id`, `has_context`, `paymaster`, `metadata`) are written as zero |
| `mint` is ABI-compatible with `IMinigameToken::mint` | Existing call sites and the `minigame::mint` helper work unchanged; unsupported params are rejected loudly, never silently ignored |
| Game contract is the authority | Games gate dead/finished runs themselves and call `refresh_metadata` (ERC-4906) after actions |

## Interface (IMinigameTokenLite)

**Interface ID:** `IMINIGAME_TOKEN_LITE_ID = 0x3ea3d599077fbe09ddbe82ff33c1abc87aef52d8609d8bf3508fdba8dd92056`

Defined in `packages/interfaces/src/token/lite.cairo`. The initializer also
registers `IMINIGAME_TOKEN_ID` so `MinigameComponent::initializer` (which
hard-asserts it and then queries `game_registry_address()`) accepts a lite
token; `game_registry_address()` always returns zero.

| Method | Cost | Notes |
| --- | --- | --- |
| `mint(...)` | 1 minter-map read (warm), optional name write, ERC721 mint | Same 15-arg signature as the full token |
| `assert_owner_and_playable(token_id, expected_owner)` | 1 storage read (owner) | Combined guard — replaces `owner_of` + `assert_is_playable` (two calls) with one |
| `is_playable` / `assert_is_playable` | 0 storage reads | Lifecycle window only — no game_over latch |
| `token_metadata`, `settings_id`, `minted_by`, `is_soulbound` | 0 storage reads | Pure unpack of the token id |
| `player_name`, `minted_by_address` | 1 storage read | |
| `refresh_metadata(_batch)` | event only | Same advisory/no-existence-check semantics as the full token |
| `update_player_name` | owner-gated write | |

Not present (reverts with ENTRYPOINT_NOT_FOUND): `update_game`, all other
`*_batch` views, `mint_batch_recipients`, objectives/settings/context/
renderer/skills/enumerable surfaces.

## Composition

Requires: `ERC721Component`, `SRC5Component`, an `OptionalMinter` impl
(`MinterComponent::MinterOptionalImpl` — minter ids gate reward claims in
consumers), and an `ERC721HooksTrait` (enforce soulbound in `before_update`
via `unpack_soulbound` — pure, no storage).

See `tests/examples/token_lite_contract.cairo` for a full wiring example.

## Testing

```bash
snforge test -p game_components_embeddable_game_standard "::token_lite::"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Re-export from interfaces package (single source of truth)
pub use game_components_interfaces::token::lite::{
IMINIGAME_TOKEN_LITE_ID, IMinigameTokenLite, IMinigameTokenLiteDispatcher,
IMinigameTokenLiteDispatcherTrait,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Token lite package tests

mod examples;
mod test_gas_bench;
mod test_token_lite;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod token_lite_contract;
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Example "denshokan lite" deployment: single-game ERC721 with the lite core,
// minter tracking, and a soulbound transfer guard. No registry, no enumerable,
// no objectives/context/skills/renderer extensions, no mutable token state.
//
// A production deployment would additionally override `token_uri` to call its
// game renderer contract (one stored address, one call) and add
// Ownable/Upgradeable — omitted here to keep the example focused on the
// component wiring.

#[starknet::contract]
pub mod TokenLiteContract {
use core::num::traits::Zero;
use openzeppelin_introspection::src5::SRC5Component;
use openzeppelin_token::erc721::ERC721Component;
use starknet::ContractAddress;
use crate::token::extensions::minter::minter::MinterComponent;
use crate::token::structs::unpack_soulbound;
use crate::token_lite::token_lite_component::CoreTokenLiteComponent;

component!(path: ERC721Component, storage: erc721, event: ERC721Event);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
component!(path: CoreTokenLiteComponent, storage: core_token_lite, event: CoreTokenLiteEvent);
component!(path: MinterComponent, storage: minter, event: MinterEvent);

#[storage]
struct Storage {
#[substorage(v0)]
erc721: ERC721Component::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage,
#[substorage(v0)]
core_token_lite: CoreTokenLiteComponent::Storage,
#[substorage(v0)]
minter: MinterComponent::Storage,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC721Event: ERC721Component::Event,
#[flat]
SRC5Event: SRC5Component::Event,
#[flat]
CoreTokenLiteEvent: CoreTokenLiteComponent::Event,
#[flat]
MinterEvent: MinterComponent::Event,
}

#[abi(embed_v0)]
impl ERC721Impl = ERC721Component::ERC721Impl<ContractState>;
#[abi(embed_v0)]
impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl<ContractState>;
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
#[abi(embed_v0)]
impl CoreTokenLiteImpl =
CoreTokenLiteComponent::CoreTokenLiteImpl<ContractState>;
#[abi(embed_v0)]
impl MinterImpl = MinterComponent::MinterImpl<ContractState>;

impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;
impl CoreTokenLiteInternalImpl = CoreTokenLiteComponent::InternalImpl<ContractState>;
impl MinterInternalImpl = MinterComponent::InternalImpl<ContractState>;

// Minter is the only optional feature the lite core consumes.
impl MinterOptionalImpl = MinterComponent::MinterOptionalImpl<ContractState>;

impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
fn before_update(
ref self: ERC721Component::ComponentState<ContractState>,
to: ContractAddress,
token_id: u256,
auth: ContractAddress,
) {
// Soulbound is a bit in the token id — pure unpack, no storage.
// Only transfers are blocked; mints (owner == 0) and burns
// (to == 0) pass through.
let current_owner = self._owner_of(token_id);
if !current_owner.is_zero() && !to.is_zero() {
if unpack_soulbound(token_id.try_into().unwrap()) {
panic!("Token is soulbound and cannot be transferred");
}
}
}

fn after_update(
ref self: ERC721Component::ComponentState<ContractState>,
to: ContractAddress,
token_id: u256,
auth: ContractAddress,
) {}
}

#[constructor]
fn constructor(
ref self: ContractState,
name: ByteArray,
symbol: ByteArray,
base_uri: ByteArray,
game_address: ContractAddress,
) {
self.erc721.initializer(name, symbol, base_uri);
self.core_token_lite.initializer(game_address);
self.minter.initializer();
}
}
Loading
Loading