Problem
Currently, creating subdomains under a SuiNS domain requires possession of the parent SuinsRegistration NFT. This presents challenges for delegation scenarios:
- No delegation without transfer: Domain owners cannot grant subdomain creation rights without transferring the entire NFT
- All-or-nothing permissions: There's no way to grant limited permissions (e.g., only leaf subdomains, or only up to N subdomains)
- No revocation mechanism: Once the NFT is transferred, the original owner loses all control
- Re-registration invalidation: Delegated permissions should automatically invalidate when a domain expires and is re-registered by someone else
Proposal
Introduce SubnameCap, a native capability object that allows holders to create subdomains under a specific parent domain without possessing the parent SuinsRegistration NFT.
Key Design: NFT ID Binding + Active Caps List
The SubnameCap stores both parent_domain AND parent_nft_id. Validation requires the cap to be present in an active caps list stored as a dynamic field on the main SuiNS shared object (the protocol singleton), following the same pattern used by Registry and Config:
| Scenario |
NFT ID |
In Active List |
Cap Valid? |
| Domain transfer (same NFT → new wallet) |
Unchanged |
Yes |
✅ Yes (persists) |
| Domain expires + re-registered |
New NFT ID |
Orphaned |
❌ No (auto-invalidated) |
| Explicit revocation by NFT holder |
Unchanged |
Removed |
❌ No (revoked) |
| Cap surrendered by holder |
N/A |
Removed |
❌ No (destroyed) |
| Batch clear by NFT holder |
Unchanged |
All removed |
❌ No (all revoked) |
SubnameCap Structure
public struct SubnameCap has key, store {
id: UID,
parent_domain: Domain,
parent_nft_id: ID,
// Permissions
allow_leaf_creation: bool,
allow_node_creation: bool,
default_node_allow_creation: bool,
default_node_allow_extension: bool,
// Programmable Limits
max_uses: Option<u64>, // None = unlimited
uses_count: u64,
max_duration_ms: Option<u64>, // Duration limit for created subdomains
cap_expiration_ms: Option<u64>, // Absolute timestamp when cap expires
}
Active Caps Storage
Active caps are stored as a dynamic field on the main SuiNS shared object (the protocol singleton), keyed by domain. Access is gated through the existing app authorization system:
// Only authorized apps can access the UID for dynamic fields
let suins_uid = suins::app_uid_mut(SubDomains {}, suins);
df::add(suins_uid, ActiveCapsKey { domain }, ActiveSubnameCaps { caps: vector[] });
/// Key for the dynamic field (defined in subdomains module, so only it can access)
public struct ActiveCapsKey has copy, drop, store {
domain: Domain,
}
/// Metadata stored for each active cap (enables enumeration without cap object)
public struct CapEntry has copy, drop, store {
cap_id: ID,
created_at_ms: u64,
allow_leaf: bool,
allow_node: bool,
max_uses: Option<u64>,
max_duration_ms: Option<u64>,
cap_expiration_ms: Option<u64>,
}
/// Container stored as dynamic field value
public struct ActiveSubnameCaps has store {
caps: vector<CapEntry>,
}
Storage hierarchy:
SuiNS (shared object - protocol singleton)
├── RegistryKey<Registry> → Registry (existing)
├── ConfigKey<Config> → Config (existing)
├── AppKey<App> → bool (existing)
└── ActiveCapsKey { domain } → ActiveSubnameCaps (new, via app_uid_mut)
├── { domain: "example.sui" } → vector<CapEntry>
├── { domain: "foo.sui" } → vector<CapEntry>
└── ...
This approach:
- Follows established SuiNS patterns (Registry, Config stored the same way)
- Uses existing app authorization (
app_uid / app_uid_mut require authorized app witness)
- Enables enumeration of all active caps for a domain via
get_active_caps(suins, domain)
- No additional shared object contention (subdomain ops already require
&mut SuiNS)
- Key type isolation ensures only the subdomains module can access its data
- Automatically orphans old caps when domain is re-registered (NFT ID check fails)
Core Functions
| Function |
Requires |
Description |
create_subname_cap |
&mut SuiNS, parent NFT |
Creates cap and adds to active list |
revoke_subname_cap |
&mut SuiNS, parent NFT, cap_id |
Removes cap from active list (one-sided) |
surrender_subname_cap |
&mut SuiNS, cap object |
Cap holder removes and destroys their cap |
clear_active_caps |
&mut SuiNS, parent NFT |
Batch revoke ALL caps for a domain |
new_leaf_with_cap |
&mut SuiNS, &mut SubnameCap |
Create leaf subdomain with cap |
new_with_cap |
&mut SuiNS, &mut SubnameCap |
Create node subdomain with cap |
Note: Functions require &mut SuiNS because the active caps list is stored as a dynamic field on the SuiNS object. This is consistent with existing subdomain operations which already require &mut SuiNS for registry access.
Validation Flow
When a cap is used (new_leaf_with_cap or new_with_cap):
- Check
allow_leaf_creation or allow_node_creation permission
- Verify parent domain exists in registry
- Verify parent is not expired
- Verify
record.nft_id() == cap.parent_nft_id (auto-invalidates on re-registration)
- Verify cap ID is in the active caps list for that domain
- Check cap-specific limits (max_uses, cap_expiration_ms, max_duration_ms)
Events
For indexing and transparency:
SubnameCapCreated - Emitted when a cap is created
SubnameCapRevoked - Emitted when a cap is revoked by parent holder
SubnameCapSurrendered - Emitted when a cap is surrendered by cap holder
SubnameCapUsed - Emitted when a cap is used to create a subdomain
ActiveCapsCleared - Emitted when all caps are batch cleared for a domain
Enumeration & Query Functions
// Active caps enumeration
get_active_caps(suins, domain): vector<CapEntry>
get_active_caps_count(suins, domain): u64
is_cap_active(suins, cap): bool
is_cap_revoked(suins, cap): bool
// CapEntry getters (for reading from active list without cap object)
cap_entry_id(entry): ID
cap_entry_created_at_ms(entry): u64
cap_entry_allow_leaf(entry): bool
cap_entry_allow_node(entry): bool
cap_entry_max_uses(entry): Option<u64>
cap_entry_max_duration_ms(entry): Option<u64>
cap_entry_expiration_ms(entry): Option<u64>
// SubnameCap getters
cap_parent_domain(cap): Domain
cap_parent_nft_id(cap): ID
cap_allow_leaf_creation(cap): bool
cap_allow_node_creation(cap): bool
cap_max_uses(cap): Option<u64>
cap_uses_count(cap): u64
cap_remaining_uses(cap): Option<u64>
cap_max_duration_ms(cap): Option<u64>
cap_expiration_ms(cap): Option<u64>
is_cap_expired(cap, clock): bool
is_cap_usage_exhausted(cap): bool
Files Modified
-
packages/suins/sources/suins.move
- Add
app_uid<App: drop>(_: App, self: &SuiNS): &UID - read-only UID access for authorized apps
- Add
app_uid_mut<App: drop>(_: App, self: &mut SuiNS): &mut UID - mutable UID access for authorized apps
- These functions use
assert_app_is_authorized<App>() to ensure only authorized apps can access
-
packages/subdomains/sources/subdomains.move
- Add SubnameCap struct and related types
- Add all cap management functions
- Add events for cap lifecycle
-
packages/subdomains/tests/subdomain_tests.move
- 57 comprehensive tests including all cap functionality
Acceptance Criteria
Use Cases
- Community subdomain programs: Projects can delegate subdomain creation to community managers
- Subdomain marketplaces: Sellers can create caps with specific limits for buyers
- Organization hierarchies: Departments can manage their own subdomains
- Time-limited campaigns: Create caps that expire after an event
- Quota-based delegation: Limit delegatees to creating N subdomains
- Domain transfers: Use
clear_active_caps before transferring to revoke all delegations
Security Considerations
- NFT ID binding: Auto-invalidates caps when domain is re-registered
- Active list validation: Caps must be in active list to be used
- App authorization:
app_uid / app_uid_mut require authorized app witness, preventing unauthorized dynamic field access
- Key type isolation:
ActiveCapsKey is defined in subdomains module, so only it can access the data
- One-sided revocation: NFT holder can revoke without needing the cap object
- Batch clear: NFT holder can revoke all caps at once before domain transfer
- Surrender option: Cap holders can cleanly remove themselves from the active list
- Expiration inheritance: Subdomain expiration cannot exceed parent expiration
- Denylist compliance: Cap-created subdomains still respect the blocklist
- Limit enforcement: Uses count tracked on-chain via mutable cap reference
- Orphaned lists: On re-registration, old active caps list is orphaned but harmless (NFT ID check fails)
Problem
Currently, creating subdomains under a SuiNS domain requires possession of the parent
SuinsRegistrationNFT. This presents challenges for delegation scenarios:Proposal
Introduce
SubnameCap, a native capability object that allows holders to create subdomains under a specific parent domain without possessing the parentSuinsRegistrationNFT.Key Design: NFT ID Binding + Active Caps List
The
SubnameCapstores bothparent_domainANDparent_nft_id. Validation requires the cap to be present in an active caps list stored as a dynamic field on the main SuiNS shared object (the protocol singleton), following the same pattern used by Registry and Config:SubnameCap Structure
Active Caps Storage
Active caps are stored as a dynamic field on the main SuiNS shared object (the protocol singleton), keyed by domain. Access is gated through the existing app authorization system:
Storage hierarchy:
This approach:
app_uid/app_uid_mutrequire authorized app witness)get_active_caps(suins, domain)&mut SuiNS)Core Functions
create_subname_cap&mut SuiNS, parent NFTrevoke_subname_cap&mut SuiNS, parent NFT, cap_idsurrender_subname_cap&mut SuiNS, cap objectclear_active_caps&mut SuiNS, parent NFTnew_leaf_with_cap&mut SuiNS,&mut SubnameCapnew_with_cap&mut SuiNS,&mut SubnameCapNote: Functions require
&mut SuiNSbecause the active caps list is stored as a dynamic field on the SuiNS object. This is consistent with existing subdomain operations which already require&mut SuiNSfor registry access.Validation Flow
When a cap is used (
new_leaf_with_capornew_with_cap):allow_leaf_creationorallow_node_creationpermissionrecord.nft_id() == cap.parent_nft_id(auto-invalidates on re-registration)Events
For indexing and transparency:
SubnameCapCreated- Emitted when a cap is createdSubnameCapRevoked- Emitted when a cap is revoked by parent holderSubnameCapSurrendered- Emitted when a cap is surrendered by cap holderSubnameCapUsed- Emitted when a cap is used to create a subdomainActiveCapsCleared- Emitted when all caps are batch cleared for a domainEnumeration & Query Functions
Files Modified
packages/suins/sources/suins.moveapp_uid<App: drop>(_: App, self: &SuiNS): &UID- read-only UID access for authorized appsapp_uid_mut<App: drop>(_: App, self: &mut SuiNS): &mut UID- mutable UID access for authorized appsassert_app_is_authorized<App>()to ensure only authorized apps can accesspackages/subdomains/sources/subdomains.movepackages/subdomains/tests/subdomain_tests.moveAcceptance Criteria
SubnameCapstruct withkey, storeabilities (transferable)get_active_caps,get_active_caps_count)Use Cases
clear_active_capsbefore transferring to revoke all delegationsSecurity Considerations
app_uid/app_uid_mutrequire authorized app witness, preventing unauthorized dynamic field accessActiveCapsKeyis defined in subdomains module, so only it can access the data