This document describes the read-only capability flags exposed by the Aegis RWA Contracts. They advertise which modules are enabled and which protocol behaviours are supported by a given deployment, so SDK and dashboard clients can feature-gate their UI from a single call.
Like docs/error-codes.md numeric codes and
docs/events.md topics, the capability keys and field names
below are a stable contract: match on field/key name, never on struct
declaration order or Rust type layout.
Not a permission check. A capability says the protocol implements a behaviour, not that you may perform it or that it will succeed right now. Authorization is still governed by
docs/admin-roles.md, and per-investor eligibility bydocs/investor-eligibility.md. Never use a capability flag as an access-control decision.
Before this change, a front-end had no way to ask the contract what it could do. Clients had three bad options:
- Hardcode a feature matrix per deployment — silently wrong the moment a contract is upgraded or a second deployment ships with a different build.
- Probe entrypoints and catch the revert — expensive, noisy, and
indistinguishable from a genuine authorization or state failure. A missing
approveand an unauthorizedapprovelook identical off-chain. - Parse the contract spec XDR — workable for a bespoke tool, but it only
reveals that a function exists, not whether the behaviour behind it is
actually implemented.
distribute_yieldexists but settles nothing on-chain; a spec dump cannot tell you that.
The capability read helper answers the question directly, in one call, from the deployment itself.
Two kinds of field appear in the response and must not be conflated:
| Kind | Fields | Caching |
|---|---|---|
| Static capability | every CapabilityStatus field, and the module_enabled booleans |
Fixed for a given contract build. Safe to cache for the lifetime of a deployment. |
| Runtime switch | initialized, paused, asset_active, operations_enabled, supply_cap_enforced, holding_cap_enforced, metadata_configured |
Derived from current ledger state; can change between calls. Re-read; do not cache. |
The distinction matters. pause.global_pause is Supported even while the
contract is paused — the capability exists; it is simply currently active.
The runtime flag pause.paused is what tells you operations are halted.
A client that gates on the wrong one will hide its pause banner exactly when
it needs to show it.
Every behaviour flag is a tri-state, not a bool. A plain boolean cannot
distinguish "this contract will never do that" from "not built yet, but
tracked" — and front-ends need that distinction to choose between hiding a
control permanently and rendering a "coming soon" affordance.
| Status | Meaning | Recommended client behaviour |
|---|---|---|
Supported |
Implemented and callable against this deployment now. | Render the feature normally. |
Planned |
Not available yet; a known, documented gap a future version is expected to close. | Render a disabled / "coming soon" control. Never build a transaction against it. |
Unsupported |
Not available, and not a tracked gap — deliberately out of scope, or impossible under the protocol's design. | Hide the corresponding UI entirely. |
The following are Unsupported and, importantly, why — so integrators do
not file them as bugs or wait for them:
| Capability | Why it is Unsupported |
|---|---|
minting.burning |
No burn entrypoint exists. Supply is monotonically increasing; a lowered supply cap blocks future mints rather than burning existing units (see supply-cap-governance.md). |
| compliance.investor_tiers | DataKey::Whitelist is a single boolean carrying no jurisdiction, accreditation tier, or investor-class data. Regime-specific segmentation (e.g. Reg D vs. Reg S) is off-chain only — see threat-model.md C-4. |
| events.transfer_restriction_events | Structurally impossible. Soroban discards events from a reverted invocation, so a blocked transfer can never durably publish one. Watch the granular restriction error codes (3004, 4000, 4001, 7000–7004) or call check_transfer_restriction instead — see events.md. |
| compliance.investor_tiers | The compliance lifecycle models compliance state (Unknown/Pending/Approved/Revoked/Blocked), not investor class. It carries no jurisdiction or accreditation-tier data, so regime-specific segmentation (e.g. Reg D vs. Reg S) is off-chain only — see threat-model.md C-4. |
| events.transfer_restriction_events | Structurally impossible. Soroban discards events from a reverted invocation, so a blocked transfer can never durably publish one. Watch error codes 3004/4000/4001 instead — see events.md. |
Planned items — allowances, transfer_from, transfer_fees,
decimals, sep41_token_interface,
yield_distribution, and asset_registered_event — correspond to the gaps
tracked in
dashboard-readiness-review.md and the
// TODO: markers in the source.
All three functions are pure reads: no storage writes, no events, no
authorization required, and they never panic — including before
initialize has been called and while the contract is paused. They are safe
to call from a read-only RPC simulation at any time.
Returns the full descriptor.
pub struct ContractCapabilities {
pub capability_version: u32, // schema version of this response
pub contract_version: String, // crate version, e.g. "0.1.0"
pub initialized: bool, // runtime: has initialize() been called
pub rbac: CapabilityStatus,
pub two_step_governance: CapabilityStatus,
pub sep41_token_interface: CapabilityStatus,
pub compliance: ComplianceCapabilities,
pub minting: MintingCapabilities,
pub transfers: TransferCapabilities,
pub pause: PauseCapabilities,
pub metadata: MetadataCapabilities,
pub events: EventCapabilities,
}| Field | Type | Default | Meaning |
|---|---|---|---|
module_enabled |
bool |
true |
Compliance module compiled in. |
whitelist |
status | Supported |
whitelist_user. |
whitelist_revocation |
status | Supported |
revoke_whitelist. |
batch_whitelisting |
status | Supported |
Many addresses per invocation via batch_set_compliance_status. |
batch_status_updates |
status | Supported |
Atomic multi-address lifecycle updates via batch_set_compliance_status. |
investor_tiers |
status | Unsupported |
Jurisdiction/accreditation tiers. |
lifecycle_states |
status | Supported |
Five-state compliance lifecycle + get_compliance_status. See compliance-lifecycle.md. |
lifecycle_transitions |
status | Supported |
Enforced transition matrix on set_compliance_status, plus the pre-flight transition reads. |
transition_guards |
status | Supported |
Pre-flight transition guards (check_compliance_transition, get_compliance_transition_guard, check_compliance_batch) returning a typed refusal reason. See compliance-transition-guards.md. |
eligibility_reads |
status | Supported |
get_investor_eligibility, check_transfer_eligibility. |
enforced_on_mint |
bool |
true |
Every mint checks the receiver's lifecycle status. |
enforced_on_transfer |
bool |
true |
Every transfer checks both parties' lifecycle statuses. |
| Field | Type | Default | Meaning |
|---|---|---|---|
module_enabled |
bool |
true |
Minting module compiled in. |
minting |
status | Supported |
mint_asset. |
burning |
status | Unsupported |
No burn entrypoint. |
supply_cap |
status | Supported |
Global cap with 2-step governance. |
supply_cap_enforced |
bool (runtime) |
false |
A cap is currently active (> 0). |
yield_distribution |
status | Planned |
distribute_yield emits an event only; it settles no value on-chain. |
issuer_separation |
status | Supported |
Issuer separation-of-duties controls. See issuer-role-separation.md. |
issuer_separation_enforced |
bool |
false |
Runtime: whether the separation policy is currently enforced. |
| Field | Type | Default | Meaning |
|---|---|---|---|
module_enabled |
bool |
true |
Transfer module compiled in. |
transfers |
status | Supported |
transfer. |
holding_cap |
status | Supported |
Per-investor cap with 2-step governance. |
holding_cap_enforced |
bool (runtime) |
false |
A holding cap is currently active (> 0). |
allowances |
status | Planned |
SEP-41 approve / allowance. |
transfer_from |
status | Planned |
SEP-41 transfer_from. |
transfer_fees |
status | Planned |
Fee deduction on transfer. |
transfer_eligibility_check |
status | Supported |
check_transfer_eligibility. |
transfer_restriction_reasons |
status | Supported |
check_transfer_restriction, check_mint_restriction, get_restriction_code — granular blocked-transfer reason codes. See transfer-restrictions.md. |
| Field | Type | Default | Meaning |
|---|---|---|---|
module_enabled |
bool |
true |
Pause module compiled in. |
global_pause |
status | Supported |
pause / unpause. |
paused |
bool (runtime) |
false |
Contract is currently globally paused. |
asset_lifecycle |
status | Supported |
set_asset_status. |
asset_active |
bool (runtime) |
true |
Lifecycle status is Active. |
operations_enabled |
bool (runtime, derived) |
true |
!paused && asset_active. When false, no mint or transfer can succeed for any investor. |
operations_enabled is a protocol-level switch only. An investor may still
be individually ineligible while it is true — use
get_investor_eligibility for the per-address answer.
| Field | Type | Default | Meaning |
|---|---|---|---|
module_enabled |
bool |
true |
Metadata module compiled in. |
name_and_symbol |
status | Supported |
Readable/writable name and ticker. |
metadata_uri |
status | Supported |
Off-chain metadata URI pointer. |
decimals |
status | Planned |
SEP-41 decimals. Do not infer a precision. |
metadata_configured |
bool (runtime) |
false |
A non-empty name and symbol have been set. |
lifecycle_restricted |
bool |
true |
Updates are blocked in Retired/Blocked. |
Mirrors the topics in events.md. compliance_events,
compliance_lifecycle_events, minting_events, transfer_events,
admin_events, governance_events, and asset_lifecycle_events are all
Supported;
transfer_restriction_events is Unsupported and asset_registered_event
is Planned.
Resolves a single key, derived from the same descriptor so the two can never disagree.
Unknown keys resolve to Unsupported rather than reverting. This is
deliberate: a newer SDK probing an older deployment fails safe and simply
hides the feature, instead of the call trapping and the dashboard rendering
an error.
Registry (also returned by get_capability_keys()):
| Key | Resolves to |
|---|---|
rbac |
rbac |
two_step_governance |
two_step_governance |
sep41 |
sep41_token_interface |
compliance |
compliance.module_enabled |
whitelist |
compliance.whitelist |
whitelist_revocation |
compliance.whitelist_revocation |
batch_whitelisting |
compliance.batch_whitelisting |
compliance_batch_updates |
compliance.batch_status_updates |
investor_tiers |
compliance.investor_tiers |
compliance_lifecycle |
compliance.lifecycle_states |
compliance_transitions |
compliance.lifecycle_transitions |
compliance_transition_guards |
compliance.transition_guards |
eligibility_reads |
compliance.eligibility_reads |
minting |
minting.minting |
burning |
minting.burning |
supply_cap |
minting.supply_cap |
yield_distribution |
minting.yield_distribution |
issuer_separation |
minting.issuer_separation |
transfers |
transfers.transfers |
holding_cap |
transfers.holding_cap |
allowances |
transfers.allowances |
transfer_from |
transfers.transfer_from |
transfer_fees |
transfers.transfer_fees |
transfer_eligibility |
transfers.transfer_eligibility_check |
transfer_restriction_reasons |
transfers.transfer_restriction_reasons |
pause |
pause.global_pause |
asset_lifecycle |
pause.asset_lifecycle |
metadata |
metadata.name_and_symbol |
metadata_uri |
metadata.metadata_uri |
decimals |
metadata.decimals |
events |
events.module_enabled |
compliance_lifecycle_events |
events.compliance_lifecycle_events |
transfer_restriction_events |
events.transfer_restriction_events |
asset_registered_event |
events.asset_registered_event |
Returns every key this contract version understands, so a client can enumerate the registry rather than hardcode it — and detect at runtime that a deployment is older or newer than the keys it knows about. Order is stable within a schema version.
check_interface_compatibility(client_schema_version, required_capabilities) -> InterfaceCompatibilityReport
Checks a client's required capability keys against this deployment in one
call and reports the schema-version relationship, so an SDK or dashboard can
answer "can I safely integrate with this deployment?" without hand-rolling
the comparison. See docs/interface-compatibility.md
for the full field reference and usage guidance.
capability_version is the schema version of the response
(CAPABILITY_SCHEMA_VERSION, currently 5 — last bumped when the
minting.issuer_separation fields and the issuer_separation registry key
were added); contract_version is the deployed crate's semantic version.
Bump capability_version whenever a field is added to any capability
struct or a key is added to the registry, so an SDK pinned to an older schema
can detect that the deployment may advertise capabilities it does not know
about.
Fields and keys are append-only. Never remove or repurpose an existing
one — downstream clients may have it hardcoded. Flipping a status from
Planned to Supported when a feature actually ships is the expected
lifecycle and does not require a schema bump; adding the field in the first
place does.
Clients should treat an unrecognised capability_version as "newer than
me": read the fields they know, ignore the rest, and fall back to
Unsupported for anything absent.
- Feature-gate navigation at load. Call
get_capabilities()once on app init and cache the static fields. Hide the "Approvals" tab whiletransfers.allowances != Supported; hide any burn control whileminting.burning == Unsupported. - Render
Planneddifferently fromUnsupported.Planned→ a disabled control with a "coming soon" tooltip.Unsupported→ no control at all. Never build a transaction against a non-Supportedcapability, even if the entrypoint appears in the contract spec. - Do not cache runtime switches. Re-read
paused,operations_enabled,*_enforced, andmetadata_configuredon each view. Usepause.operations_enabledfor a global "trading halted" banner, thenget_investor_eligibilityfor the per-investor reason. - Gate cap indicators on the runtime flag. Only show a "X of Y capacity
used" meter when
transfers.holding_cap_enforcedistrue; otherwiseremaining_capacityisNoneand there is no ceiling to render. - Fall back on unconfigured metadata. When
metadata_configuredisfalse, render a placeholder rather than blank strings, and never infer a decimal precision whilemetadata.decimalsisPlanned. - Show a setup state when
initializedisfalse. Every privileged entrypoint will revert withNotInitialized(2000) until it flips. - Probing is safe.
supports_capabilitywith an unknown key returnsUnsupported, so a client can safely ask about features that may not exist yet without special-casing the error path. - All three are ordinary read calls — invoke them like
get_balance_of/is_whitelistedviasoroban contract invoke(read-only, no signing) or the generated SDK client's simulate-only path.
- Purely additive. No existing function, error code, event, or storage
key changed.
asset::get_asset_status_internalwas widened from private topubso the capability module reads lifecycle state through the same helpermint_asset/transferuse — behaviour is unchanged, and there is now one source of truth rather than a duplicated default. - No new storage keys and no new error codes. Every read falls back to the same safe default its owning module uses, which is why the helper cannot panic on an uninitialized contract.
- Not a state-changing call, so it is exempt from the pause guard by
design — consistent with the other read helpers documented in
contract-spec.md. - Tests covering the default capability state (before and after
initialize), the no-mutation guarantee, paused and lifecycle states, active caps, metadata configuration, explicit unsupported/planned states, unknown-key fail-safe behaviour, and registry/descriptor agreement live insrc/test.rs.