Access Control
Dubhe provides three complementary access control mechanisms: package-level write isolation via DappKey, runtime guards for admin and version checks, and an emergency pause flag.
DappKey: storage namespace isolation
Every DApp has a DappKey struct generated in sources/codegen/dapp_key.move:
// auto-generated
module mygame::dapp_key;
public struct DappKey has copy, drop {}
public(package) fun new(): DappKey { DappKey {} }This empty struct has one important property: its fully-qualified type name is unique per package:
0xPKG_ADDRESS::dapp_key::DappKeyThe Framework uses this type name as a namespace prefix for all storage operations. Two DApps with different package addresses will always write to completely separate storage slots, even if they share the same DappHub.
dapp_key::new() is public(package) — it can only be called from within the same package. External packages cannot produce a valid DappKey instance, which means they cannot write to your DApp’s storage.
public(package) write isolation
All generated write functions are public(package):
// In sources/codegen/resources/level.move (auto-generated)
public(package) fun set(user_storage: &mut UserStorage, value: u32, ctx: &mut TxContext) {
// ...
}public(package) means only modules in the same Move package can call this function. External packages cannot call level::set directly, regardless of what arguments they provide. This prevents:
- Other DApps from writing into your namespace
- External scripts from bypassing your system contract logic
Your system contracts in sources/systems/ are in the same package, so they can freely call generated write functions. External contracts can only call your public entry functions.
Read functions are fully public — any contract can read from any DApp’s storage:
// Reading is open — no access restriction
public fun get(user_storage: &UserStorage): u32 { ... }
public fun has(user_storage: &UserStorage): bool { ... }Exposing functions to external callers
If you want to allow external contracts to trigger writes in your DApp, wrap the logic in a public entry function in your system contract:
// sources/systems/level_system.move — you write this
module mygame::level_system;
use dubhe::dapp_service::{DappStorage, UserStorage};
use mygame::level;
// Accessible from PTBs and external contracts
public entry fun level_up(
dapp_storage: &DappStorage,
user_storage: &mut UserStorage,
ctx: &mut TxContext,
) {
let lv = level::get(user_storage);
level::set(user_storage, lv + 1, ctx); // calls public(package) internally
}The public entry function is your controlled interface. All state changes go through it, and you apply any guards you need before touching storage.
Runtime guard: admin check
Use ensure_dapp_admin to restrict a function to the DApp admin:
use dubhe::dapp_service::{DappStorage, UserStorage};
use dubhe::dapp_system;
use mygame::dapp_key::DappKey;
use mygame::level;
public entry fun admin_reset_player(
dapp_storage: &DappStorage,
user_storage: &mut UserStorage,
ctx: &mut TxContext,
) {
// Aborts with NO_PERMISSION if caller is not the DApp admin
dapp_system::ensure_dapp_admin<DappKey>(dapp_storage, ctx.sender());
level::delete(user_storage, ctx);
}The admin is recorded in DappStorage at DApp creation (the deployer address). See DApp Admin for how to change it.
Runtime guard: version check
After an upgrade, block calls from old package versions:
use mygame::migrate;
use dubhe::dapp_system;
use dubhe::dapp_service::{DappStorage, UserStorage};
use mygame::dapp_key::DappKey;
public entry fun level_up(
dapp_storage: &DappStorage,
user_storage: &mut UserStorage,
ctx: &mut TxContext,
) {
// Aborts if this package's version != on-chain version
dapp_system::ensure_latest_version<DappKey>(dapp_storage, migrate::on_chain_version());
level::set(user_storage, level::get(user_storage) + 1, ctx);
}ensure_latest_version reads the version recorded in DappStorage (set by upgrade_dapp) and compares it to the ON_CHAIN_VERSION constant compiled into the current package. If a user sends a transaction through an old package version, the constant will not match and the call aborts.
Best practice: Add
ensure_latest_versionto everypublic entrywrite function. Read functions can safely omit it.
Runtime guard: pause check
The DApp admin can pause a DApp to halt all writes during emergencies:
// Admin pauses the DApp
dapp_system::set_paused<DappKey>(&mut dapp_storage, true, ctx);
// In your system functions, guard against writes while paused:
public entry fun level_up(
dapp_storage: &DappStorage,
user_storage: &mut UserStorage,
ctx: &mut TxContext,
) {
dapp_system::ensure_not_paused<DappKey>(dapp_storage); // aborts if paused
level::set(user_storage, level::get(user_storage) + 1, ctx);
}
// Admin resumes
dapp_system::set_paused<DappKey>(&mut dapp_storage, false, ctx);Read functions are typically not guarded by the pause flag — users should still be able to query state while the DApp is paused.
Recommended guard order
When a function needs lifecycle protection, apply guards in this order:
public entry fun my_action(
dapp_storage: &DappStorage,
user_storage: &mut UserStorage,
ctx: &mut TxContext,
) {
// 1. Version check — block stale package callers
dapp_system::ensure_latest_version<DappKey>(dapp_storage, migrate::on_chain_version());
// 2. Pause check — respect emergency stop
dapp_system::ensure_not_paused<DappKey>(dapp_storage);
// 3. Business logic guards (custom errors)
error::player_not_found(level::has(user_storage));
// 4. State changes
// ...
}None of these guards are mandatory — they are opt-in protections. A simple function like counter::inc that has no version or pause requirements needs none of them. Add only the guards that match what your function needs to enforce.
CLI lint check:
dubhe buildanddubhe testprint a warning for anypublic entry funthat acceptsDappStoragebut lacksensure_latest_version.dubhe publishanddubhe upgradeshow an interactive confirmation prompt in the same case. This catches missing guards before deployment without blocking development.
Consequences of omitting guards
Guards must be added before the first publish. Because Sui packages are immutable once deployed, a function that ships without a guard can never be retroactively protected — even after an upgrade.
Missing ensure_latest_version
After dubhe upgrade --bump-version or a schema migration, the on-chain version
advances. But if a function has no version check, the old package version of that
function remains fully callable forever:
Old package 0xAAAA → attack() has no ensure_latest_version
New package 0xBBBB → attack() has ensure_latest_version ✅
User calls 0xAAAA::attack() → executes with OLD (buggy) logic, no abort
User calls 0xBBBB::attack() → executes with NEW (fixed) logic ✅Upgrading the package does not prevent access to the old package. Both exist permanently on-chain. Without the guard, an attacker can always route calls to the old package ID and trigger the old code.
Missing ensure_not_paused
set_paused sets a flag in DappStorage, but calling it has no effect on any function
that does not check the flag. If a buggy function lacks ensure_not_paused, the
operator’s emergency pause cannot stop it:
Admin calls set_paused(..., true) → paused flag is set ✅
User calls 0xAAAA::buggy_fn() → no ensure_not_paused, so flag is ignored,
call executes normally ❌When both guards are absent
The operator has no on-chain lever to stop a buggy function that was published without either guard. The only options are:
- Situational: if the function’s execution path depends on some other shared object state you control, engineer that state to cause the old call to abort naturally (rare and case-by-case).
- Accept the loss: disclose the exploit, halt deposits/withdrawals through front-end controls, and migrate assets to a new contract deployment.
The one partial fallback
If a function has ensure_not_paused but not ensure_latest_version, the operator
can still halt activity by pausing — even after an upgrade advances the version.
ensure_not_paused is a real-time kill switch that works regardless of version state.
This is why both guards together provide meaningfully stronger protection than either
alone:
| Guards present | Upgrade blocks old calls | Pause blocks old calls |
|---|---|---|
| Neither | ❌ | ❌ |
ensure_not_paused only | ❌ | ✅ (while paused) |
ensure_latest_version only | ✅ (after migration) | ❌ |
| Both | ✅ (after migration) | ✅ (while paused) |