This document describes the role-based access control (RBAC) system for the Aegis RWA Protocol smart contracts.
| Role | Description | Privileged Operations |
|---|---|---|
Admin |
Supreme authority. Can perform all operations and manage roles. | All operations + role management + admin transfer |
ComplianceOfficer |
Manages the compliance whitelist and lifecycle. | whitelist_user, revoke_whitelist, set_compliance_status, batch_set_compliance_status |
AssetManager |
Manages asset minting and yield distribution. | mint_asset, distribute_yield |
EmergencyOfficer |
Compliance privileges plus the emergency pause. Not an issuer: mint_asset and distribute_yield call require_role(AssetManager), which admits only an AssetManager or the admin. |
whitelist_user, revoke_whitelist, set_compliance_status, batch_set_compliance_status, pause |
None |
No role assigned. Cannot perform any privileged operation. | None (except transfer which requires self-auth) |
The supreme admin (DataKey::Admin) bypasses all role checks. The require_role helper function checks if the caller holds the required role OR is the admin, and reverts if neither condition is met.
Assigns a role to target. Only the admin can call this.
- Requires admin authentication (
require_auth) - Requires the caller to be the current admin
- Cannot assign the
Adminrole — usetransfer_adminfor safe handoff - Emits a
RoleAssignedEvent
Revokes the role from target, setting it to Role::None. Only the admin can call this.
- Requires admin authentication
- Reverts if the target has no role assigned
- Emits a
RoleRevokedEvent
Returns the role assigned to address, or Role::None if unassigned.
Initiates a 2-step admin transfer. Sets candidate as the pending new admin.
- Requires admin authentication
- Stores the candidate in
DataKey::AdminCandidate - Emits an
AdminTransferInitiatedEvent
Completes a 2-step admin transfer. The candidate must call this to accept.
- Requires candidate authentication
- Reverts if no pending transfer exists or if the caller is not the candidate
- Transfers the
Adminrole from the previous admin to the candidate - Emits an
AdminTransferredEvent
The admin can renounce their own role. This is irreversible.
- Requires admin authentication
- Removes the admin from storage and sets their role to
Role::None - Emits an
AdminTransferredEvent(self-renounced)
All role changes emit Soroban events for off-chain indexing and audit trails:
| Event | Topic | Payload |
|---|---|---|
| Contract initialized | ("contract_initialized",) |
{ admin } |
| Role assigned | ("role_assigned",) |
{ admin, target, role } |
| Role revoked | ("role_revoked",) |
{ admin, target, role } |
| Admin transfer initiated | ("admin_transfer_initiated",) |
{ current_admin, candidate } |
| Admin transferred | ("admin_transferred",) |
{ previous_admin, new_admin } |
| Admin renounced | ("admin_renounced",) |
{ previous_admin, new_admin } |
| Contract paused | ("contract_paused",) |
{ admin } |
| Contract unpaused | ("contract_unpaused",) |
{ admin } |
| Issuer separation policy updated | ("issuer_separation_policy_updated",) |
{ admin, previous_policy, new_policy } |
See events.md for the full event schema reference and
SDK/dashboard compatibility notes.
- The
initializefunction sets the first admin and grants them theAdminrole. - The admin should then assign roles to trusted addresses using
set_role.
- Assigning roles: The admin calls
set_rolewith the target address and desired role. Each address can hold exactly one role. - Revoking roles: The admin calls
remove_roleto revoke a role. The target reverts toRole::None. - Role upgrades: To change a role, revoke the current role first, then assign the new one.
The 2-step transfer pattern prevents accidental or malicious admin loss:
- Step 1: Current admin calls
transfer_admin(admin, candidate). - Step 2: The candidate calls
accept_admin(candidate).
If the candidate does not accept, the transfer can be superseded by a new transfer_admin call with a different candidate.
- The admin can immediately revoke any role using
remove_role. - The admin can renounce their own role using
renounce_admin(irreversible). - In case of a compromised admin key, the admin should transfer to a new key immediately.
| Operation | Required Role | Admin Bypass |
|---|---|---|
mint_asset |
AssetManager |
Yes |
distribute_yield |
AssetManager |
Yes |
whitelist_user |
ComplianceOfficer |
Yes |
revoke_whitelist |
ComplianceOfficer |
Yes |
set_compliance_status |
ComplianceOfficer* |
Yes |
batch_set_compliance_status |
ComplianceOfficer* |
Yes |
set_issuer_separation_policy |
Admin |
N/A (admin-only) |
set_role |
Admin |
N/A (admin-only) |
remove_role |
Admin |
N/A (admin-only) |
transfer |
Self-auth | N/A |
transfer_admin |
Admin |
N/A (admin-only) |
accept_admin |
Candidate auth | N/A |
* Moving an address out of Blocked is admin-only, including inside
batch_set_compliance_status.
Roles say which privileges an address holds; duties say which classes of
decision it can make. Only the admin currently carries both the compliance and
the issuance duty, which means one key can clear an investor and then fund
them. issuer-role-separation.md specifies the
duty map, the opt-in policy that forbids that combination (plus self-issuance
and same-approver issuance), the check_issuance_authority pre-flight read,
and the assumptions the controls rest on.
| Role | Compliance | Issuance | Emergency | Governance |
|---|---|---|---|---|
ComplianceOfficer |
✓ | |||
AssetManager |
✓ | |||
EmergencyOfficer |
✓ | ✓ | ||
Admin |
✓ | ✓ | ✓ | ✓ |
| Key | Storage Type | Description |
|---|---|---|
DataKey::Admin |
Instance | The supreme admin address |
DataKey::AdminCandidate |
Instance | Pending admin during 2-step transfer |
DataKey::Role(Address) |
Persistent | The role assigned to an address |
DataKey::IssuerSeparationPolicy |
Instance | Issuer separation-of-duties policy (absent = permissive default) |
DataKey::ComplianceApprover(Address) |
Persistent | Address that last approved an investor's compliance |
DataKey::Whitelist(Address) |
Persistent | Whitelist flag (legacy, kept for compatibility) |
DataKey::Balance(Address) |
Persistent | Token balance |
DataKey::TotalSupply |
Instance | Global total supply |