Cycle

The Scriptorium

Smart Assembly code templates and tools for on-chain development in Eve Frontier.

Tribe-Gated Storage
IntermediateChapter 4 of 515 min read

Tribe Verification

The tribe verification system is what makes AncientStorage a tribe-gated extension rather than a free-for-all shared inventory. Every share() and withdraw() call checks that the calling player belongs to the correct tribe before proceeding.

The Data Structures

Tribe configuration is stored as a dynamic field on ExtensionConfig using two structs:

move
public struct TribeConfigKey has copy, drop, store {}

public struct TribeConfig has drop, store {
    tribe: u32,
}

TribeConfigKey

This is the dynamic field key. It is an empty struct used purely for type-level indexing. Its abilities tell us:

  • copy -- can be duplicated (needed because we use it in both existence checks and lookups)
  • drop -- can be silently discarded after use
  • store -- can be stored as a dynamic field key

Since TribeConfigKey has no fields, there is exactly one meaningful value: TribeConfigKey {}. This means there can be only one TribeConfig per ExtensionConfig -- each deployment gates for a single tribe.

TribeConfig

This is the dynamic field value. It holds the tribe ID as a u32:

  • drop -- allows the old config to be discarded when overwritten via set_rule
  • store -- required to be stored as a dynamic field value

Setting the Tribe Configuration

The admin calls set_tribe_config after deployment to specify which tribe is allowed:

move
public fun set_tribe_config(
    extension_config: &mut ExtensionConfig,
    admin_cap: &AdminCap,
    tribe_id: u32,
) {
    config::set_rule<TribeConfigKey, TribeConfig>(
        extension_config,
        admin_cap,
        TribeConfigKey {},
        TribeConfig { tribe: tribe_id },
    );
}

This function:

  • Takes a mutable reference to ExtensionConfig (shared object, so this triggers consensus).
  • Requires &AdminCap proof (only the admin can set the tribe).
  • Calls config::set_rule which handles both insert and update (upsert pattern).
  • If a TribeConfig already exists, set_rule removes the old one (it has drop, so it is silently discarded) and adds the new one. This lets the admin change the tribe at any time.

    Calling from TypeScript

    typescript
    const tx = new Transaction();
    
    tx.moveCall({
      target: ${TRIBE_STORAGE_PACKAGE_ID}::tribe_storage::set_tribe_config,
      arguments: [
        tx.object(TRIBE_STORAGE_CONFIG_ID),  // ExtensionConfig (shared)
        tx.object(adminCapId),                // AdminCap (owned by admin)
        tx.pure.u32(5),                       // tribe ID (e.g., tribe 5)
      ],
    });
    
    await signAndExecute({ transaction: tx });

    Reading the Tribe Configuration

    A public view function exposes the configured tribe ID:

    move
    public fun tribe(extension_config: &ExtensionConfig): u32 {
        assert!(
            config::has_rule<TribeConfigKey>(extension_config, TribeConfigKey {}),
            ENoTribeConfig,
        );
        config::borrow_rule<TribeConfigKey, TribeConfig>(
            extension_config,
            TribeConfigKey {},
        ).tribe
    }

    This function:

  • Asserts that a TribeConfig dynamic field exists. If not, it aborts with ENoTribeConfig ("Tribe configuration not set on ExtensionConfig").
  • Borrows the TribeConfig value and returns its tribe field.
  • Anyone can call this -- it only requires an immutable reference to the shared config.

    The verify_tribe() Function

    This is the internal function that gates all player operations:

    move
    fun verify_tribe(extension_config: &ExtensionConfig, character: &Character) {
        assert!(
            config::has_rule<TribeConfigKey>(extension_config, TribeConfigKey {}),
            ENoTribeConfig,
        );
        let tribe_cfg = config::borrow_rule<TribeConfigKey, TribeConfig>(
            extension_config,
            TribeConfigKey {},
        );
        assert!(character.tribe() == tribe_cfg.tribe, ENotTribeMember);
    }

    Step by Step

  • Check config exists: has_rule verifies that set_tribe_config has been called. If not, the assertion fails with error code 1 (ENoTribeConfig). This protects against using the extension before it is configured.
  • Read the tribe ID: borrow_rule returns a reference to the stored TribeConfig. This is a read-only operation on the shared ExtensionConfig.
  • Compare with character's tribe: character.tribe() is a function from the EVE Frontier Character module that returns the character's tribe ID as a u32. If it does not match the configured tribe, the assertion fails with error code 0 (ENotTribeMember).
  • Why It Is Private

    verify_tribe has no visibility modifier, making it private to the tribe_storage module. It is an internal validation function -- not something external callers should invoke directly. It is called at the top of every player-facing function:
    move
    public fun share<T: key>(
        extension_config: &ExtensionConfig,
        storage_unit: &mut StorageUnit,
        character: &Character,
        // ...
    ) {
        verify_tribe(extension_config, character);  // <-- first thing
        // ... rest of logic
    }
    
    public fun withdraw(
        extension_config: &ExtensionConfig,
        storage_unit: &mut StorageUnit,
        character: &Character,
        // ...
    ) {
        verify_tribe(extension_config, character);  // <-- first thing
        // ... rest of logic
    }

    By checking at the top of each function, the transaction aborts immediately if the tribe check fails -- no gas is wasted on subsequent operations.

    Error Handling in Practice

    When tribe verification fails, the transaction error includes the human-readable message:

    ``

    MoveAbort(0, "Character is not in the configured tribe")

    `

    or:

    `

    MoveAbort(1, "Tribe configuration not set on ExtensionConfig")

    `

    The dApp's TypeScript code catches these errors and displays them to the user, helping them understand why the operation was rejected.

    Multi-Tribe Extensions

    The current design supports only one tribe per deployment. To support multiple tribes, you could:

  • Deploy multiple times: Each deployment gets its own ExtensionConfig with a different tribe ID. Simple but requires separate SSU authorization for each.
  • Change the data model: Replace TribeConfig { tribe: u32 } with TribeConfig { tribes: vector } and update verify_tribe to check membership in the list.
  • Use tribe-keyed dynamic fields: Instead of TribeConfigKey (singleton), use TribeConfigKey { tribe: u32 } to store per-tribe rules. This is more complex but allows fine-grained per-tribe configuration.
  • Key Takeaways

    • TribeConfigKey and TribeConfig are a dynamic field key-value pair storing the allowed tribe ID.
    • set_tribe_config() is an admin-only function that upserts the tribe configuration.
    • verify_tribe() is a private function called at the start of every player operation. It aborts if the tribe config is missing or the character's tribe does not match.
    • Error messages use the #[error]` attribute for human-readable abort messages.
    • The current design supports one tribe per deployment. Multi-tribe support would require data model changes.

    Sign in to track your progress.