Skip to content
Closed
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
255 changes: 255 additions & 0 deletions .agents/tasks/plan-check.md

Large diffs are not rendered by default.

392 changes: 392 additions & 0 deletions .agents/tasks/plan.md

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@ path = "tests/integration/full_flow.rs"
name = "security_auth_tests"
path = "tests/integration/security_auth_tests.rs"

[[test]]
name = "security_arithmetic_tests"
path = "tests/unit/security_arithmetic_tests.rs"

[[test]]
name = "security_reentrancy_tests"
path = "tests/integration/security_reentrancy_tests.rs"
Expand All @@ -90,8 +86,8 @@ name = "pause_event_consistency"
path = "tests/integration/pause_event_consistency.rs"

[[test]]
name = "emergency_pause_flow"
path = "tests/integration/emergency_pause_flow.rs"
name = "pause_flow"
path = "tests/integration/pause_flow.rs"

[[test]]
name = "token_vesting_flow"
Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,30 @@ A learner is eligible for a credential when:
| `is_initialized` | — | — | `bool` | Whether `initialize()` has been called. |
| `get_storage_size` | — | — | `u64` | Number of persistent storage entries. |

#### Admin

| Function | Auth | Parameters | Returns | Description |
|---|---|---|---|---|
| `admin` | — | — | `Address` | Admin address. |
| `has_role` | — | `address: Address, role: AdminRole` | `bool` | Check role assignment. |
| `grant_role` | Admin | `caller: Address, address: Address, role: AdminRole` | — | Grant a role. |
| `revoke_role` | Admin | `caller: Address, address: Address, role: AdminRole` | — | Revoke a role. |
| `upgrade` | Admin | `caller: Address, new_wasm_hash: BytesN<32>` | — | Upgrade contract WASM. |
| `wasm_hash` | — | — | `Option<BytesN<32>>` | Current WASM hash. |
| `transfer_admin` | Admin | `new_admin: Address` | — | Initiate delayed admin transfer. |
| `accept_admin` | Pending | — | — | Accept admin role after delay. |
| `cancel_admin_transfer` | Admin | — | — | Cancel pending admin transfer. |
| `pending_admin` | — | — | `Option<PendingAdminTransfer>` | Pending transfer details. |
| `admin_transfer_delay` | — | — | `u64` | Current delay in seconds. |
| `set_admin_transfer_delay` | Admin | `delay_seconds: u64` | — | Set the delay (minimum 3,600 seconds; panics below). |

#### Emergency Controls

| Function | Auth | Parameters | Returns | Description |
|---|---|---|---|---|
| `pause` | Pauser | `caller: Address` | — | Pause all state-changing operations. |
| `unpause` | Pauser | `caller: Address` | — | Resume operations. |

### learn-token

#### Initialization
Expand Down Expand Up @@ -527,7 +551,7 @@ that matters (e.g. each voter) before its balance changes, (3) read with `balanc

| Function | Auth | Parameters | Returns | Description |
|---|---|---|---|---|
| `mint_credential` | Admin + Learner | `to: Address, course_id: Symbol, score: u32, metadata_uri: Symbol` | `u64` | Mint a credential NFT. Score-gated (>=50) and verified on-chain. |
| `mint_credential` | Minter + Learner | `caller: Address, to: Address, course_id: Symbol, score: u32, metadata_uri: Symbol` | `u64` | Mint a credential NFT. Score-gated (>=50) and verified on-chain. |
| `verify_credential` | — | `credential_id: u64` | `CredentialInfo` | Verify a credential and return its info. |
| `verify_credential_with_display` | — | `credential_id: u64` | `CredentialVerification` | Verify with optional display properties. |
| `revoke_credential` | Admin | `credential_id: u64` | — | Revoke a credential. |
Expand Down Expand Up @@ -571,6 +595,12 @@ that matters (e.g. each voter) before its balance changes, (3) read with `balanc
|---|---|---|---|---|
| `admin` | — | — | `Address` | Admin address. |
| `progress_tracker` | — | — | `Address` | Progress-tracker address. |
| `has_role` | — | `role: AdminRole, address: Address` | `bool` | Check role assignment. |
| `grant_role` | Admin | `caller: Address, role: AdminRole, address: Address` | — | Grant a role. |
| `revoke_role` | Admin | `caller: Address, role: AdminRole, address: Address` | — | Revoke a role. |
| `upgrade` | Admin | `caller: Address, new_wasm_hash: BytesN<32>` | — | Upgrade contract WASM. |
| `wasm_hash` | — | — | `Option<BytesN<32>>` | Current WASM hash. |
| `upgrade_version` | — | — | `u32` | Number of upgrades performed. |
| `transfer_admin` | Admin | `new_admin: Address` | — | Initiate delayed admin transfer. |
| `accept_admin` | Pending | — | — | Accept admin role after delay. |
| `cancel_admin_transfer` | Admin | — | — | Cancel pending admin transfer. |
Expand All @@ -585,8 +615,8 @@ that matters (e.g. each voter) before its balance changes, (3) read with `balanc

| Function | Auth | Parameters | Returns | Description |
|---|---|---|---|---|
| `emergency_pause` | Admin | — | — | Pause all state-changing operations. |
| `unpause` | Admin | — | — | Resume operations. |
| `pause` | Pauser | `caller: Address` | — | Pause all state-changing operations. |
| `unpause` | Pauser | `caller: Address` | — | Resume operations. |

## Data Types

Expand Down
36 changes: 35 additions & 1 deletion contracts/credential-nft/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
//!
//! Mirrors the `learn-token` event module so the three contracts emit a
//! consistent set of operational events (#422). Pause events previously had no
//! home here, so `emergency_pause`/`unpause` were the only admin operations in
//! home here, so `pause`/`unpause` were the only admin operations in
//! this contract that monitoring tooling could not observe.

use soroban_sdk::{Address, Env, Symbol};

use crate::metadata::AdminRole;

// ── Event Indexing Convention (#548) ─────────────────────────────────────────
//
// Every event below puts its event-name `Symbol` in `topics[0]` (unchanged
Expand Down Expand Up @@ -230,3 +232,35 @@ pub fn admin_transfer_cancelled(env: &Env, current_admin: &Address, new_admin: &
);
env.events().publish(topics, (current_admin.clone(),));
}

// ── Role Management Events (#551) ────────────────────────────────────────────

/// Emitted when a role is granted to an address (#551).
///
/// Topics: ["role_granted", address]
/// Data: (role_symbol,)
pub fn role_granted(env: &Env, address: &Address, role: &AdminRole) {
let role_str = match role {
AdminRole::Admin => "Admin",
AdminRole::Minter => "Minter",
AdminRole::Pauser => "Pauser",
};
let topics = (Symbol::new(env, "role_granted"), address.clone());
env.events()
.publish(topics, (Symbol::new(env, role_str),));
}

/// Emitted when a role is revoked from an address (#551).
///
/// Topics: ["role_revoked", address]
/// Data: (role_symbol,)
pub fn role_revoked(env: &Env, address: &Address, role: &AdminRole) {
let role_str = match role {
AdminRole::Admin => "Admin",
AdminRole::Minter => "Minter",
AdminRole::Pauser => "Pauser",
};
let topics = (Symbol::new(env, "role_revoked"), address.clone());
env.events()
.publish(topics, (Symbol::new(env, role_str),));
}
Loading