The Credence access control module provides reusable, composable role-based access control modifiers for smart contracts. It implements three primary roles (Admin, Verifier, Identity Owner) with support for role composition and comprehensive security event logging.
- Purpose: Full administrative privileges over the contract
- Capabilities:
- Initialize contract configuration
- Add/remove verifiers
- Execute slashing operations
- Modify system parameters
- Storage: Single admin address stored at key
"admin" - Modifier:
require_admin(e: &Env, caller: &Address)
- Purpose: Validate and verify identity claims
- Capabilities:
- Verify identity attestations
- Review and approve claims
- Participate in validation workflows
- Storage: Per-verifier boolean flag with key prefix
"verifier" - Modifier:
require_verifier(e: &Env, caller: &Address) - Management: Admin can add/remove verifiers
- Purpose: Self-sovereign control over personal identity and bonds
- Capabilities:
- Manage own bonds
- Withdraw funds
- Update personal identity data
- Modifier:
require_identity_owner(e: &Env, caller: &Address, expected: &Address) - Validation: Direct address comparison
The system supports combining roles for flexible access patterns:
require_admin_or_verifier(e: &Env, caller: &Address)This modifier allows either admin OR verifier to proceed, enabling shared responsibilities while maintaining clear audit trails.
The bond contract uses explicit authorization at each state-changing entrypoint.
| Entrypoint | Scope | Required auth |
|---|---|---|
create_bond(identity, ...) |
Owner-scoped | identity.require_auth() |
withdraw(...) |
Owner-scoped | Stored bond owner: bond.identity.require_auth() |
withdraw_early(...) |
Owner-scoped | Stored bond owner: bond.identity.require_auth() |
top_up(...) |
Owner-scoped | Stored bond owner: bond.identity.require_auth() |
extend_duration(...) |
Owner-scoped | Stored bond owner: bond.identity.require_auth() |
request_withdrawal(...) |
Owner-scoped | Stored bond owner: bond.identity.require_auth() |
renew_if_rolling(...) |
Owner-scoped | Stored bond owner: bond.identity.require_auth() |
withdraw_bond(identity) |
Owner-scoped | identity.require_auth() and bond.identity == identity |
slash(...) / slash_bond(...) |
Admin-only | Stored admin check plus admin.require_auth() |
set_early_exit_config(...) |
Admin-only | Stored admin check plus admin.require_auth() |
register_attester(...) / unregister_attester(...) |
Admin-only | Stored admin require_auth() |
set_attester_stake(...) / set_weight_config(...) |
Admin-only | Stored admin check plus admin.require_auth() |
collect_fees(...) |
Admin-only | Stored admin check plus admin.require_auth() |
Enforces admin-only access.
Panics:
"not admin"- Caller is not the admin"not initialized"- Contract not initialized
Events: Emits access_denied on failure
Example:
pub fn set_config(e: Env, caller: Address, value: u32) {
require_admin(&e, &caller);
// Admin-only logic
}Enforces verifier-only access.
Panics: "not verifier" - Caller is not a registered verifier
Events: Emits access_denied on failure
Example:
pub fn verify_claim(e: Env, verifier: Address, claim_id: u64) {
require_verifier(&e, &verifier);
// Verifier-only logic
}Enforces identity owner access.
Panics: "not identity owner" - Caller doesn't match expected identity
Events: Emits access_denied on failure
Example:
pub fn withdraw(e: Env, caller: Address, bond: &IdentityBond) {
require_identity_owner(&e, &caller, &bond.identity);
// Owner-only logic
}Enforces admin OR verifier access (role composition).
Panics: "not authorized" - Caller is neither admin nor verifier
Events: Emits access_denied on failure
Example:
pub fn review_submission(e: Env, reviewer: Address, submission_id: u64) {
require_admin_or_verifier(&e, &reviewer);
// Admin or verifier logic
}Add a new verifier (admin only).
Events: Emits verifier_added with verifier address
Example:
pub fn add_verifier(e: Env, admin: Address, verifier: Address) {
add_verifier_role(&e, &admin, &verifier);
}Remove an existing verifier (admin only).
Events: Emits verifier_removed with verifier address
Example:
pub fn remove_verifier(e: Env, admin: Address, verifier: Address) {
remove_verifier_role(&e, &admin, &verifier);
}Check if an address is the admin (non-panicking).
Returns: true if address is admin, false otherwise
Check if an address is a registered verifier (non-panicking).
Returns: true if address is a verifier, false otherwise
Get the current admin address.
Panics: "not initialized" if contract not initialized
Emitted when access control check fails.
Topics: ("access_denied",)
Data: (caller: Address, role: Symbol, error_code: u32)
Error Codes:
1- NotAdmin2- NotVerifier3- NotIdentityOwner4- NotInitialized
Emitted when a verifier is added.
Topics: ("verifier_added",)
Data: (verifier: Address,)
Emitted when a verifier is removed.
Topics: ("verifier_removed",)
Data: (verifier: Address,)
use crate::access_control::require_admin;
pub fn set_penalty_rate(e: Env, admin: Address, rate_bps: u32) {
require_admin(&e, &admin);
// Validate rate
if rate_bps > 10000 {
panic!("rate exceeds 100%");
}
// Store configuration
e.storage().instance().set(&Symbol::new(&e, "penalty_rate"), &rate_bps);
}use crate::access_control::require_verifier;
pub fn approve_attestation(e: Env, verifier: Address, attestation_id: u64) {
require_verifier(&e, &verifier);
// Load attestation
let key = Symbol::new(&e, "attestation");
let mut attestation: Attestation = e.storage().instance().get(&key)
.unwrap_or_else(|| panic!("attestation not found"));
// Mark as verified
attestation.verified = true;
attestation.verifier = verifier.clone();
e.storage().instance().set(&key, &attestation);
e.events().publish((Symbol::new(&e, "attestation_approved"),), attestation_id);
}use crate::access_control::require_identity_owner;
pub fn update_profile(e: Env, caller: Address, new_data: ProfileData) {
// Load existing profile
let key = Symbol::new(&e, "profile");
let profile: Profile = e.storage().instance().get(&key)
.unwrap_or_else(|| panic!("profile not found"));
// Verify ownership
require_identity_owner(&e, &caller, &profile.owner);
// Update profile
let updated = Profile {
owner: profile.owner,
data: new_data,
updated_at: e.ledger().timestamp(),
};
e.storage().instance().set(&key, &updated);
}use crate::access_control::require_admin_or_verifier;
pub fn flag_suspicious_activity(e: Env, reporter: Address, identity: Address, reason: Symbol) {
// Either admin or verifier can flag
require_admin_or_verifier(&e, &reporter);
// Record the flag
let flag = SuspiciousActivityFlag {
identity,
reporter: reporter.clone(),
reason,
timestamp: e.ledger().timestamp(),
};
e.storage().instance().set(&Symbol::new(&e, "flag"), &flag);
e.events().publish((Symbol::new(&e, "activity_flagged"),), flag);
}All access control failures emit access_denied events for security monitoring and audit trails. Monitor these events to detect:
- Unauthorized access attempts
- Potential security breaches
- Misconfigured permissions
- Admin role has ultimate authority - protect admin keys carefully
- Verifiers should be trusted entities with limited scope
- Identity owners control only their own data
Always initialize the contract with initialize(e: Env, admin: Address) before using access control functions. Uninitialized contracts will panic on admin checks.
- Only admin can add/remove verifiers
- Removed verifiers immediately lose access
- Consider implementing verifier rotation policies
The system prevents privilege escalation:
- Verifiers cannot become admin
- Identity owners cannot grant themselves verifier status
- Only admin can modify the verifier set
The module includes comprehensive tests covering:
✅ Admin-only access control
✅ Verifier-only access control
✅ Identity owner access control
✅ Role composition (admin OR verifier)
✅ Unauthorized access scenarios
✅ Access denial event emissions
✅ Multiple verifiers management
✅ Verifier addition/removal
✅ Edge cases (re-adding verifiers, uninitialized state)
✅ Event verification for all operations
Coverage Note: Access-control paths are covered by dedicated unit tests, including unauthorized and event-emission scenarios.
Project-wide numeric coverage requires an external coverage tool (for example cargo-llvm-cov), which is not bundled in this repository.
Run tests with:
cargo test --package credence_bond test_access_controluse crate::access_control::{require_admin, require_verifier, require_identity_owner};
#[contract]
pub struct CredenceIdentity;
#[contractimpl]
impl CredenceIdentity {
pub fn initialize(e: Env, admin: Address) {
e.storage().instance().set(&Symbol::new(&e, "admin"), &admin);
}
pub fn add_verifier(e: Env, admin: Address, verifier: Address) {
add_verifier_role(&e, &admin, &verifier);
}
pub fn verify_identity(e: Env, verifier: Address, identity: Address) {
require_verifier(&e, &verifier);
// Verification logic
}
pub fn update_identity(e: Env, caller: Address, identity: Address, data: IdentityData) {
require_identity_owner(&e, &caller, &identity);
// Update logic
}
pub fn emergency_freeze(e: Env, admin: Address, identity: Address) {
require_admin(&e, &admin);
// Emergency freeze logic
}
}Every privileged admin entrypoint that accepts a target Address MUST
reject the zero/invalid address sentinel. The sentinel is the strkey
encoding of the all-zero Ed25519 public key:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Assigning a role to — or transferring ownership to — this address can
permanently strand governance, so the guard is enforced before any
authorisation or storage mutation. The rejection uses
ContractError::InvalidAdminAddress (code 110).
| Entrypoint | Parameter guarded |
|---|---|
add_admin |
new_admin |
update_admin_role |
admin_address |
transfer_ownership |
new_owner |
reactivate_admin |
admin_address |
deactivate_admin |
admin_address |
remove_admin |
admin_to_remove |
set_pause_signer |
signer |
An address is considered invalid when its string representation exactly equals the sentinel above. This is a purely syntactic check: it catches uninitialised / garbage strkeys. A valid (non-sentinel) contract or account address is never rejected by this guard.
Potential improvements for future versions:
- Time-based Roles: Roles with expiration timestamps
- Multi-sig Admin: Require multiple admin signatures for critical operations
- Role Hierarchies: More granular permission levels
- Delegation: Temporary permission delegation
- Rate Limiting: Prevent abuse by limiting operations per time period
- Role Metadata: Store additional context about role assignments
The following table summarizes every privileged/admin entrypoint in the Bond Contract that requires authorization to prevent regressions. They are exhaustively checked through a data-driven negative matrix in the test suite to ensure:
- non-admin rejection coverage (unauthorized callers are rejected)
- uninitialized coverage (uninitialized contract execution fails)
- real auth enforcement coverage (genuine
require_authenforcement without test mocks)
| Entrypoint | Type | Scope |
|---|---|---|
set_early_exit_config |
Admin-only | Updates early exit parameters and sets treasury. |
register_attester |
Admin-only | Adds a new attester to the contract. |
unregister_attester |
Admin-only | Removes an existing attester from the contract. |
set_attester_stake |
Admin-only | Sets specific attester stake amount. |
set_weight_config |
Admin-only | Configures weighting multipliers and limits. |
slash |
Admin-only | Fully or partially slashes a bond and distributes fees. |
slash_bond |
Admin-only | Internal/variant wrapper to perform bond slashing. |
collect_fees |
Admin-only | Collects all accumulated protocol fees. |
Note: Any new admin entrypoint must be appended to this matrix and included in the PrivilegedCase test array.