Skip to content
Merged
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
69 changes: 69 additions & 0 deletions contracts/reputation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal,
Symbol, Vec,
};
pub use profile::BadgeLevel;

mod profile;
mod storage;
Expand Down Expand Up @@ -480,6 +481,7 @@ impl ReputationContract {
let total_jobs = metrics.completed_jobs;
let badge_level = metrics.badge_level;

profile.refresh_badges();
storage::write_profile(&env, &address, &profile);
env.events().publish(
("reputation", "ScoreAdjusted"),
Expand Down Expand Up @@ -512,6 +514,7 @@ impl ReputationContract {
let total_jobs = metrics.completed_jobs;
let badge_level = metrics.badge_level;

profile.refresh_badges();
storage::write_profile(&env, &address, &profile);
env.events().publish(
("reputation", "ScoreAdjusted"),
Expand Down Expand Up @@ -955,6 +958,72 @@ mod test {
assert_eq!(saved_hash, Some(hash));
}

// ── Issue #402: badge minting ──

#[test]
fn test_badge_starts_at_bronze_for_default_score() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let addr = Address::generate(&env);
let cid = env.register_contract(None, ReputationContract);
let client = ReputationContractClient::new(&env, &cid);
client.initialize(&admin);

// Default score is 5000 → Bronze
let badge = client.get_badge(&addr, &Role::Freelancer);
assert_eq!(badge, BadgeLevel::Bronze);
}

#[test]
fn test_badge_upgrades_to_silver_at_6000() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let addr = Address::generate(&env);
let cid = env.register_contract(None, ReputationContract);
let client = ReputationContractClient::new(&env, &cid);
client.initialize(&admin);

// Raise score by 1000 → 5000+1000 = 6000 → Silver
client.update_score(&addr, &Role::Freelancer, &1000);
let badge = client.get_badge(&addr, &Role::Freelancer);
assert_eq!(badge, BadgeLevel::Silver);
}

#[test]
fn test_badge_upgrades_to_gold_at_8000() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let addr = Address::generate(&env);
let cid = env.register_contract(None, ReputationContract);
let client = ReputationContractClient::new(&env, &cid);
client.initialize(&admin);

client.update_score(&addr, &Role::Freelancer, &3000); // 5000+3000=8000
assert_eq!(client.get_badge(&addr, &Role::Freelancer), BadgeLevel::Gold);
}

#[test]
fn test_slash_downgrades_badge() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let addr = Address::generate(&env);
let cid = env.register_contract(None, ReputationContract);
let client = ReputationContractClient::new(&env, &cid);
client.initialize(&admin);

// Bring to Gold first, then slash twice to drop back to Bronze
client.update_score(&addr, &Role::Client, &3000); // 8000 → Gold
assert_eq!(client.get_badge(&addr, &Role::Client), BadgeLevel::Gold);
client.slash(&addr, &Role::Client, &soroban_sdk::Symbol::new(&env, "fraud")); // 6000 → Silver
assert_eq!(client.get_badge(&addr, &Role::Client), BadgeLevel::Silver);
client.slash(&addr, &Role::Client, &soroban_sdk::Symbol::new(&env, "fraud")); // 4000 → Bronze
assert_eq!(client.get_badge(&addr, &Role::Client), BadgeLevel::Bronze);
}

#[test]
#[should_panic(expected = "Error(Contract, #2)")]
fn test_upgrade_requires_admin() {
Expand Down
30 changes: 30 additions & 0 deletions contracts/reputation/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ impl RoleMetrics {
}
}

/// Badge tier awarded based on cumulative score thresholds.
/// Scores are in basis points (0–10 000).
///
/// Thresholds:
/// Bronze ≥ 4 000
/// Silver ≥ 6 000
/// Gold ≥ 8 000
/// Platinum ≥ 9 500
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BadgeLevel {
None,
Bronze,
Silver,
Gold,
Platinum,
}

impl BadgeLevel {
pub fn from_score(score: i32) -> Self {
match score {
s if s >= 9_500 => BadgeLevel::Platinum,
s if s >= 8_000 => BadgeLevel::Gold,
s if s >= 6_000 => BadgeLevel::Silver,
s if s >= 4_000 => BadgeLevel::Bronze,
_ => BadgeLevel::None,
}
}
}

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct Profile {
Expand Down
Loading