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
68 changes: 68 additions & 0 deletions contracts/badge-nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ pub struct BadgeMinted {
pub minted_at: u64,
}

#[contractevent]
pub struct BadgeRevoked {
#[topic]
pub learner: Address,
#[topic]
pub course_id: u32,
}

#[contractimpl]
impl BadgeNFT {
/// Initializes the BadgeNFT contract with the authorized registry address.
Expand Down Expand Up @@ -99,6 +107,66 @@ impl BadgeNFT {
.publish(&env);
}

/// Revokes a Soulbound Token (badge) from a learner's address.
/// Only the official protocol registry can trigger this for fraud prevention.
///
/// # Arguments
/// * `admin` - The caller address (must be the authorized registry)
/// * `learner` - The learner address to revoke the badge from
/// * `course_id` - The course ID of the badge to revoke
///
/// # Panics
/// * If caller authentication fails
/// * If caller is not the authorized registry
pub fn revoke_badge(env: Env, admin: Address, learner: Address, course_id: u32) {
// 1. admin.require_auth()
admin.require_auth();

// 2. Fetch 'Admin' (Registry) address from Instance storage. Assert caller == Admin.
let stored_admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.expect("Contract not initialized");
assert!(
admin == stored_admin,
"Unauthorized: Caller is not the authorized registry"
);

// 3. Construct DataKey::UserBadges(learner).
let badges_key = DataKey::UserBadges(learner.clone());

// 4. Fetch existing Vec<Badge>.
let mut badges: Vec<Badge> = env
.storage()
.persistent()
.get(&badges_key)
.unwrap_or_else(|| Vec::new(&env));

// 5. Find the badge with course_id and remove it.
let mut found = false;
let mut index_to_remove = 0;
for (i, badge) in badges.iter().enumerate() {
if badge.course_id == course_id {
index_to_remove = i as u32;
found = true;
break;
}
}

if found {
badges.remove(index_to_remove);
env.storage().persistent().set(&badges_key, &badges);

// 6. Emit BadgeRevoked event.
BadgeRevoked {
learner,
course_id,
}
.publish(&env);
}
}

/// Returns all badges for a specific learner.
///
/// # Arguments
Expand Down
54 changes: 54 additions & 0 deletions contracts/badge-nft/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,60 @@ fn test_mint_badge_timestamp_is_set() {
assert_eq!(badge.minted_at, 0);
}

// ── revoke_badge Tests ───────────────────────────────────────────────────────

#[test]
fn test_revoke_badge_success() {
let (env, client) = setup();
let registry = Address::generate(&env);
let learner = Address::generate(&env);

client.initialize(&registry);
client.mint_badge(&registry, &learner, &1);

// Verify badge exists
assert_eq!(client.get_badge_count(&learner), 1);

// Revoke badge
client.revoke_badge(&registry, &learner, &1);

// Verify badge is removed
assert_eq!(client.get_badge_count(&learner), 0);
}

#[test]
fn test_revoke_badge_emits_event() {
let (env, client) = setup();
let registry = Address::generate(&env);
let learner = Address::generate(&env);

client.initialize(&registry);
client.mint_badge(&registry, &learner, &1);

client.revoke_badge(&registry, &learner, &1);

let last_event = env.events().all().last().unwrap();
let expected_topic: Vec<Val> =
(Symbol::new(&env, "badge_revoked"), &learner, 1u32).into_val(&env);

assert_eq!(last_event.1, expected_topic);
}

#[test]
#[should_panic(expected = "Unauthorized: Caller is not the authorized registry")]
fn test_revoke_badge_unauthorized_caller() {
let (env, client) = setup();
let registry = Address::generate(&env);
let unauthorized_caller = Address::generate(&env);
let learner = Address::generate(&env);

client.initialize(&registry);
client.mint_badge(&registry, &learner, &1);

// Try to revoke with unauthorized caller - should panic
client.revoke_badge(&unauthorized_caller, &learner, &1);
}

// ── get_badges Tests ─────────────────────────────────────────────────────────

#[test]
Expand Down
Loading
Loading