Skip to main content
Version: next

Dapp Registry

A dapp on Casper is usually more than one contract: a token, a vault, a governance contract, a few contracts deployed later by a factory. Wallets, explorers and other tools see them as unrelated package hashes. The dapp modules of odra-modules group them: a registry contract describes the dapp and lists the contracts that belong to it, and every member contract points back to its registry, so the membership can be verified from both sides.

Overview​

The modules live in odra_modules::dapp:

ItemKindPurpose
DappRegistryBasemoduleStores the dapp metadata and the list of member contracts.
DappContractBasemoduleStores the address of the registry a contract belongs to.
OwnedDappRegistrycontractA ready-to-deploy registry: DappRegistryBase guarded by Ownable.
DappRegistryexternal contract traitThe registry interface, used to call a registry by address.
DappContractexternal contract traitThe member interface, used to call a member by address.
DappMetadatatypeName, description, website URL and icon URL of the dapp.

The registry itself always belongs to the dapp: get_dapp_contracts returns it first, and is_dapp_contract is true for its own address.

Verification​

When a contract is added, the registry calls get_dapp_registry on it and reverts with DappRegistryMismatch unless the contract returns the registry's address. Nobody can list a contract in a dapp it does not belong to, and a member cannot claim a registry that has not accepted it.

Every contract you add must therefore expose a get_dapp_registry entry point. Adding an account address reverts with NotAContract; adding a contract without that entry point fails with a VM error.

Registrars​

A contract added with is_registrar = true may add other contracts to the dapp - typically a contract that creates contracts through a factory. OwnedDappRegistry lets the owner add any contract, and a registrar add contracts that are not registrars themselves. Removing a contract also removes its registrar permission.

Authorization​

DappRegistryBase and DappContractBase do not check who calls them. Their read-only functions are entry points; the functions that change state (set_dapp_metadata, add_dapp_contract, remove_dapp_contract and set_dapp_registry) are plain Rust methods that are not exposed by the module. The contract that composes the module decides who may call them, with Ownable, AccessControl or its own rules.

Deploying a registry​

OwnedDappRegistry is the quickest way to start. The deployer becomes the owner.

use odra::host::{Deployer, HostRef, NoArgs};
use odra_modules::dapp::{DappMetadata, OwnedDappRegistry};

let mut registry = OwnedDappRegistry::deploy(&env, NoArgs);
registry.set_dapp_metadata(DappMetadata {
name: "My Dapp".to_string(),
description: "Lending on Casper".to_string(),
website_url: "https://mydapp.io".to_string(),
icon_url: "https://mydapp.io/icon.png".to_string()
});

It exposes:

  • set_dapp_metadata, remove_dapp_contract: owner only.
  • add_dapp_contract(&contract, is_registrar): the owner, or a registrar adding a contract that is not a registrar.
  • get_dapp_metadata, get_dapp_contracts, is_dapp_contract, is_dapp_registrar, get_dapp_registry.
  • get_owner, transfer_ownership, renounce_ownership from Ownable.

Writing a member contract​

Compose DappContractBase and set the registry in init. Delegate get_dapp_registry so the registry can verify the contract. Exposing set_dapp_registry is optional; if you do, guard it.

use odra::prelude::*;
use odra_modules::access::Ownable;
use odra_modules::dapp::DappContractBase;

#[odra::module]
pub struct Vault {
ownable: SubModule<Ownable>,
dapp: SubModule<DappContractBase>
}

#[odra::module]
impl Vault {
pub fn init(&mut self, registry: Address) {
let owner = self.env().caller();
self.ownable.init(owner);
self.dapp.set_dapp_registry(&registry);
}

pub fn set_dapp_registry(&mut self, registry: &Address) {
self.ownable.assert_owner(&self.env().caller());
self.dapp.set_dapp_registry(registry);
}

delegate! {
to self.dapp {
fn get_dapp_registry(&self) -> Address;
}
}
}

Then deploy the member with the registry address and add it:

let vault = Vault::deploy(&env, VaultInitArgs { registry: registry.address() });
registry.add_dapp_contract(&vault.address(), false);

assert_eq!(registry.get_dapp_contracts(), vec![registry.address(), vault.address()]);

Building your own registry​

To use other rules than a single owner, compose DappRegistryBase with another access module. Here, only accounts with a curator role manage the dapp:

use odra::prelude::*;
use odra_modules::access::{AccessControl, Role, DEFAULT_ADMIN_ROLE};
use odra_modules::dapp::{DappMetadata, DappRegistryBase};

/// Accounts with this role may add and remove contracts.
pub const CURATOR_ROLE: Role = [1u8; 32];

#[odra::module]
pub struct CuratedDappRegistry {
access_control: SubModule<AccessControl>,
dapp: SubModule<DappRegistryBase>
}

#[odra::module]
impl CuratedDappRegistry {
pub fn init(&mut self, metadata: DappMetadata) {
let admin = self.env().caller();
self.access_control.unchecked_grant_role(&DEFAULT_ADMIN_ROLE, &admin);
self.access_control.unchecked_grant_role(&CURATOR_ROLE, &admin);
// Creates the `__dapp` dictionary the registry keeps its data in.
self.dapp.init();
self.dapp.set_dapp_metadata(metadata);
}

pub fn add_dapp_contract(&mut self, dapp_contract: &Address, is_registrar: bool) {
self.access_control.check_role(&CURATOR_ROLE, &self.env().caller());
self.dapp.add_dapp_contract(dapp_contract, is_registrar);
}

pub fn remove_dapp_contract(&mut self, dapp_contract: &Address) {
self.access_control.check_role(&CURATOR_ROLE, &self.env().caller());
self.dapp.remove_dapp_contract(dapp_contract);
}

delegate! {
to self.dapp {
fn get_dapp_metadata(&self) -> DappMetadata;
fn get_dapp_contracts(&self) -> Vec<Address>;
fn is_dapp_contract(&self, dapp_contract: &Address) -> bool;
fn is_dapp_registrar(&self, dapp_contract: &Address) -> bool;
fn get_dapp_registry(&self) -> Address;
}
to self.access_control {
fn has_role(&self, role: &Role, address: &Address) -> bool;
fn grant_role(&mut self, role: &Role, address: &Address);
fn revoke_role(&mut self, role: &Role, address: &Address);
}
}
}

To let registrars add contracts to such a registry, call self.dapp.assert_dapp_registrar(&caller) for callers without the role, as OwnedDappRegistry does.

Registering contracts deployed by a factory​

A contract that deploys other contracts can register them right away, once the registry lists it as a registrar. The flow, using Odra factories:

  1. The admin adds the spawning contract with add_dapp_contract(&spawner, true).
  2. The spawner calls new_contract on a factory and passes the registry address (DappContractBase::register_address()) to the new contract's init.
  3. The spawner adds the new contract with DappContractBase::register(address), which calls add_dapp_contract(&address, false) on the registry.
  4. The registry checks that the caller is a registrar, calls get_dapp_registry on the new contract and adds it.
use odra::{prelude::*, ContractRef};
use odra_modules::dapp::DappContractBase;

#[odra::module]
pub struct DappCounterSpawner {
dapp: SubModule<DappContractBase>,
factory: Var<Address>,
spawned: Var<u32>
}

#[odra::module]
impl DappCounterSpawner {
// `init` and `get_dapp_registry` omitted.

/// The spawner must be registered in the dapp as a registrar.
pub fn spawn(&mut self) -> Address {
let factory = self.factory.get().unwrap_or_revert(self);
let n = self.spawned.get_or_default();
// The name keys the child in the factory, so each child gets its own.
let mut factory = DappCounterFactoryContractRef::new(self.env(), factory);
let (address, _) =
factory.new_contract(format!("DappCounter{}", n), self.dapp.register_address());
self.spawned.set(n + 1);
self.dapp.register(address);
address
}
}

A spawner that is not a registrar is rejected with CallerNotDappRegistrar:

let mut registry = OwnedDappRegistry::deploy(&env, NoArgs);
let factory = DappCounterFactory::deploy(&env, NoArgs);
let mut spawner = DappCounterSpawner::deploy(&env, DappCounterSpawnerInitArgs {
registry: registry.address(),
factory: factory.address()
});

assert_eq!(spawner.try_spawn(), Err(Error::CallerNotDappRegistrar.into()));
registry.add_dapp_contract(&spawner.address(), true);
let first = spawner.spawn();
assert_eq!(registry.get_dapp_contracts(), vec![registry.address(), spawner.address(), first]);
assert!(!registry.is_dapp_registrar(&first));

The whole example, with the DappCounter contract and a test, is in examples/src/factory/dapp.rs.

note

Factories work on OdraVM and on the Casper VM, so such tests run with both cargo odra test and cargo odra test -b casper.

Events and errors​

Events:

EventEmitted byFields
DappMetadataChangedregistryname, description, website_url, icon_url
DappContractAddedregistrycontract, is_registrar, registrar (the caller)
DappContractRemovedregistrycontract, registrar
DappRegistryChangedmemberprevious_registry, new_registry

Errors (odra_modules::dapp::errors::Error):

ErrorCodeWhen
NotAContract22000An account address is added, or set as a registry.
DappContractAlreadyRegistered22001The contract, or the registry itself, is already in the dapp.
DappContractNotRegistered22002Removing a contract that is not in the dapp.
DappRegistryMismatch22003The contract's get_dapp_registry returns a different address.
CallerNotDappRegistrar22004A contract that is not a registrar tries to add a contract.
DappRegistryNotSet22005get_dapp_registry is called on a member before its registry is set.
CannotRemoveDappRegistry22006Removing the registry itself.

Things to know​

  • get_dapp_contracts lists the registry first, then the members in the order they were added. Removing a contract keeps the order of the others.
  • get_dapp_contracts reads every member from storage. For a dapp with many contracts, prefer is_dapp_contract in contract code.