Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Ownership transfer failure boundaries**: Added deterministic coverage for suspended-candidate expiry and configuration-epoch overflow, asserting rejected proposals preserve ownership state, proposal metadata, epoch, and events (closes #1422).
- **`get_role` failure boundaries**: Added deterministic failure-boundary coverage for the `get_role` read entrypoint in `contracts/admin/src/lib.rs` (new `test_get_role_failure_boundaries.rs`, 16 tests). Pins valid, invalid, duplicate, and boundary inputs — unknown, removed, sentinel, and pre-initialisation addresses, dangling `AdminList` entries, suspension and its inclusive expiry instant, deactivation, committed promotion/demotion, and the paused state — all resolve deterministically. Reads never advance the config epoch or emit events, rejected mutations leave no partial state, and every failure surfaces the wire-stable `NotAdmin` (100) error. Documents the read-only and stored-role-vs-effective-authority invariants on the entrypoint itself.
- **Admin configuration epoch**: Added a monotonic `ConfigEpoch` to the Admin contract that advances exactly once per committed privileged mutation (admin role changes, suspension, ownership transfer, pause configuration, pause state transitions, and pause-proposal approvals). Exposes `get_config_epoch()` so clients can detect concurrent conflicts and retry; rejected, stale, repeated, and failed operations never advance the epoch and leave no partial state. Documented in `docs/CONFIG_EPOCH.md` and covered by `test_concurrency_race_safety.rs` (closes #1297).
- **Lease scope guard** (`require_matching_lease_scope`): defence-in-depth check that a lease's scope bitmask covers the requested operation. Adds `Lease` / `lease_op` primitives and typed `LeaseScopeMismatch` / `LeaseExpired` errors in `credence_errors` (Closes #847).
- **Expired-lease guard tests**: lock Fresh / Expiring soon / Expired behaviour for `require_no_expired_lease` (Closes #845).
Expand Down
43 changes: 41 additions & 2 deletions contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,43 @@ impl AdminContract {

// Helper functions

/// Get the role of an address (panics if not admin).
/// Get the stored role of an address.
///
/// # Determinism
/// The result is a pure function of the persisted `AdminInfo` record for
/// `address` at the current ledger. This entrypoint performs no mutation,
/// never advances [`DataKey::ConfigEpoch`], and emits no event, so it is
/// safe to call from read-only paths and from within other entrypoints.
/// Repeated reads are idempotent and observably identical.
///
/// # Boundary behaviour
/// * A known admin — active, suspended, or deactivated — resolves to
/// their stored role. Suspension and the `active` flag remove
/// *effective authority*, not the stored role; callers that need
/// "may this address act right now?" semantics must use
/// [`AdminContract::is_admin`] or [`AdminContract::has_role_at_least`].
/// * An unknown / never-registered address fails with
/// [`ContractError::NotAdmin`].
/// * An address removed by [`AdminContract::remove_admin`] fails with
/// [`ContractError::NotAdmin`] — no stale value survives removal.
/// * The zero/invalid sentinel address fails with
/// [`ContractError::NotAdmin`] (it has no `AdminInfo` record).
/// * A call before [`AdminContract::initialize`] fails with
/// [`ContractError::NotAdmin`]; a per-address read has no
/// initialisation dependency, so it never yields a bare panic.
/// * A dangling `AdminList` entry — the list still names the address but
/// its `AdminInfo` record is gone — fails with
/// [`ContractError::NotAdmin`] rather than serving stale list data.
/// * The read stays available while the contract is paused.
///
/// # Panics
/// * [`ContractError::NotAdmin`] (100) when `address` has no stored
/// `AdminInfo` record. This is a typed, wire-stable contract error —
/// never a bare `panic!` — and it echoes no storage contents beyond
/// the caller's own argument.
///
/// Covered by the focused failure-boundary tests in
/// `test_get_role_failure_boundaries.rs`.
pub fn get_role(e: Env, address: Address) -> AdminRole {
bump_instance_ttl(&e);
let admin_info: AdminInfo = e
Expand Down Expand Up @@ -2182,4 +2218,7 @@ mod test_reactivate_admin_boundaries;
mod test_concurrency_race_safety;

#[cfg(test)]
mod test_get_all_admins_failure_boundary;
mod test_atomic_rollback;

#[cfg(test)]
mod test_get_role_failure_boundaries;
Loading
Loading