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:
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 usestore-- 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 viaset_rulestore-- 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:
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:
ExtensionConfig (shared object, so this triggers consensus).&AdminCap proof (only the admin can set the tribe).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
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:
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:
TribeConfig dynamic field exists. If not, it aborts with ENoTribeConfig ("Tribe configuration not set on ExtensionConfig").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:
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
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.borrow_rule returns a reference to the stored TribeConfig. This is a read-only operation on the shared ExtensionConfig.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:
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.
Sign in to track your progress.