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
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
[workspace]
resolver = "2"
members = [
"contracts/*",
]
exclude = [
"contracts/target",
"contracts/course-registry",
"contracts/reward-pool",
]

[workspace.dependencies]
Expand Down
67 changes: 66 additions & 1 deletion contracts/reward-pool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![no_std]
use soroban_sdk::{contract, contractevent, contractimpl, Address, Env};
use soroban_sdk::{contract, contractevent, contractimpl, token, Address, Env};

pub mod types;
use types::DataKey;
Expand All @@ -21,6 +21,15 @@ pub struct SpenderAdded {
pub spender: Address,
}

#[contractevent]
pub struct RewardDistributed {
#[topic]
pub caller: Address,
#[topic]
pub learner: Address,
pub amount: i128,
}

#[contractimpl]
impl RewardPool {
/// Initializes the RewardPool contract with admin and token addresses.
Expand Down Expand Up @@ -58,6 +67,7 @@ impl RewardPool {
/// * `spender` - The contract address to whitelist
///
/// # Panics
/// * If contract is not initialized
/// * If admin does not match stored admin
/// * If admin authentication fails
pub fn add_approved_spender(env: Env, admin: Address, spender: Address) {
Expand All @@ -84,6 +94,61 @@ impl RewardPool {
// 5. Emit SpenderAdded event
SpenderAdded { spender }.publish(&env);
}

/// Distributes rewards from the pool to a learner.
///
/// # Arguments
/// * `caller` - The spender contract address (must be whitelisted)
/// * `learner` - The learner address to receive the reward
/// * `amount` - The amount of tokens to transfer
///
/// # Panics
/// * If caller authentication fails
/// * If amount is not positive
/// * If caller is not an authorized spender
/// * If contract is not initialized
pub fn distribute_reward(env: Env, caller: Address, learner: Address, amount: i128) {
// 1. caller.require_auth()
caller.require_auth();

// 2. Assert amount > 0
if amount <= 0 {
panic!("Amount must be positive");
}

// 3. Check if contract is initialized first
let token_id: Address = env
.storage()
.instance()
.get(&DataKey::Token)
.expect("Not initialized");

// 4. Construct DataKey::Spender(caller.clone())
// 5. Fetch the boolean from Persistent storage. Assert it is true
let is_authorized: bool = env
.storage()
.persistent()
.get(&DataKey::Spender(caller.clone()))
.unwrap_or(false);

if !is_authorized {
panic!("Caller is not an authorized spender");
}

// 6. Initialize token::Client::new(&env, &token_id)
let token_client = token::Client::new(&env, &token_id);

// 7. Call token_client.transfer(&env.current_contract_address(), &learner, &amount)
token_client.transfer(&env.current_contract_address(), &learner, &amount);

// 8. Emit RewardDistributed event
RewardDistributed {
caller,
learner,
amount,
}
.publish(&env);
}
}

mod test;
153 changes: 152 additions & 1 deletion contracts/reward-pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use soroban_sdk::{
testutils::{Address as _, Events},
vec, Address, Env, IntoVal, Map, Symbol,
token, vec, Address, Env, IntoVal, Map, Symbol, Val, Vec,
};

use crate::{RewardPool, RewardPoolClient};
Expand Down Expand Up @@ -162,3 +162,154 @@ fn test_add_same_spender_twice() {
client.add_approved_spender(&admin, &spender);
client.add_approved_spender(&admin, &spender);
}

// ── distribute_reward Tests ───────────────────────────────────────────────────

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

// Create and register a mock token contract
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

// Initialize the reward pool
client.initialize(&admin, &token_id.address());

// Whitelist the spender
client.add_approved_spender(&admin, &spender);

// Mint tokens to the reward pool contract
let token_client = token::StellarAssetClient::new(&env, &token_id.address());
token_client.mint(&client.address, &1000);

// Distribute reward
client.distribute_reward(&spender, &learner, &100);

// Check events immediately after distribute_reward
let last_event = env.events().all().last().unwrap();

let mut data_map = Map::new(&env);
data_map.set(Symbol::new(&env, "amount"), 100i128);
let expected_event: (Address, Vec<Val>, Val) = (
client.address,
(Symbol::new(&env, "reward_distributed"), &spender, &learner).into_val(&env),
data_map.into_val(&env),
);

// Verify events match
assert_eq!(
vec![&env, last_event.clone()],
vec![&env, expected_event.clone()]
);

// Verify learner received tokens
let learner_balance = token_client.balance(&learner);
assert_eq!(learner_balance, 100);
}

#[test]
#[should_panic(expected = "Amount must be positive")]
fn test_distribute_reward_zero_amount() {
let (env, client) = setup();
let admin = Address::generate(&env);
let spender = Address::generate(&env);
let learner = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());
client.add_approved_spender(&admin, &spender);

// Try to distribute zero amount - should panic
client.distribute_reward(&spender, &learner, &0);
}

#[test]
#[should_panic(expected = "Amount must be positive")]
fn test_distribute_reward_negative_amount() {
let (env, client) = setup();
let admin = Address::generate(&env);
let spender = Address::generate(&env);
let learner = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());
client.add_approved_spender(&admin, &spender);

// Try to distribute negative amount - should panic
client.distribute_reward(&spender, &learner, &-100);
}

#[test]
#[should_panic(expected = "Caller is not an authorized spender")]
fn test_distribute_reward_unauthorized_spender() {
let (env, client) = setup();
let admin = Address::generate(&env);
let unauthorized_spender = Address::generate(&env);
let learner = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());

// Try to distribute without being whitelisted - should panic
client.distribute_reward(&unauthorized_spender, &learner, &100);
}

#[test]
#[should_panic(expected = "Not initialized")]
fn test_distribute_reward_not_initialized() {
let (env, client) = setup();
let spender = Address::generate(&env);
let learner = Address::generate(&env);

// Try to distribute without initializing - should panic
client.distribute_reward(&spender, &learner, &100);
}

#[test]
fn test_distribute_reward_multiple_times() {
let (env, client) = setup();
let admin = Address::generate(&env);
let spender = Address::generate(&env);
let learner1 = Address::generate(&env);
let learner2 = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());
client.add_approved_spender(&admin, &spender);

let token_client = token::StellarAssetClient::new(&env, &token_id.address());
token_client.mint(&client.address, &1000);

// Distribute to multiple learners
client.distribute_reward(&spender, &learner1, &100);
client.distribute_reward(&spender, &learner2, &200);

assert_eq!(token_client.balance(&learner1), 100);
assert_eq!(token_client.balance(&learner2), 200);
}

#[test]
fn test_distribute_reward_multiple_spenders() {
let (env, client) = setup();
let admin = Address::generate(&env);
let spender1 = Address::generate(&env);
let spender2 = Address::generate(&env);
let learner = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());
client.add_approved_spender(&admin, &spender1);
client.add_approved_spender(&admin, &spender2);

let token_client = token::StellarAssetClient::new(&env, &token_id.address());
token_client.mint(&client.address, &1000);

// Both spenders can distribute
client.distribute_reward(&spender1, &learner, &100);
client.distribute_reward(&spender2, &learner, &50);

assert_eq!(token_client.balance(&learner), 150);
}
Loading