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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ target
# Local settings
.soroban
.stellar

# IDE settings
.vscode/
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ resolver = "2"
members = [
"contracts/*",
]
exclude = [
"contracts/target",
]

[workspace.dependencies]
soroban-sdk = "23"
Expand Down
40 changes: 40 additions & 0 deletions contracts/reward-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ pub struct PoolInitialized {
pub token: Address,
}

#[contractevent]
pub struct SpenderAdded {
#[topic]
pub spender: Address,
}

#[contractimpl]
impl RewardPool {
/// Initializes the RewardPool contract with admin and token addresses.
Expand Down Expand Up @@ -44,6 +50,40 @@ impl RewardPool {
// 5. Emit PoolInitialized event
PoolInitialized { admin, token }.publish(&env);
}

/// Adds a contract address to the approved spender whitelist.
///
/// # Arguments
/// * `admin` - The admin address (must match stored admin)
/// * `spender` - The contract address to whitelist
///
/// # Panics
/// * If admin does not match stored admin
/// * If admin authentication fails
pub fn add_approved_spender(env: Env, admin: Address, spender: Address) {
// 1. Fetch 'Admin' address from Instance storage
let stored_admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.expect("Not initialized");

// 2. Assert admin == stored_admin
if admin != stored_admin {
panic!("Unauthorized");
}

// 3. admin.require_auth()
admin.require_auth();

// 4. Save `true` to Persistent storage using DataKey::Spender(spender.clone())
env.storage()
.persistent()
.set(&DataKey::Spender(spender.clone()), &true);

// 5. Emit SpenderAdded event
SpenderAdded { spender }.publish(&env);
}
}

mod test;
89 changes: 88 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},
Address, Env,
vec, Address, Env, IntoVal, Map, Symbol,
};

use crate::{RewardPool, RewardPoolClient};
Expand Down Expand Up @@ -75,3 +75,90 @@ fn test_initialize_with_proper_auth() {
// Verify event was emitted
assert_eq!(env.events().all().len(), 1);
}

// ── add_approved_spender Tests ────────────────────────────────────────────────

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

// Initialize the contract
client.initialize(&admin, &token);

// Add approved spender - should succeed without panic
client.add_approved_spender(&admin, &spender);

// assert event emitted
let empty_data: Map<(), ()> = Map::new(&env);
let event = vec![
&env,
(
client.address,
(Symbol::new(&env, "spender_added"), spender).into_val(&env),
empty_data.into_val(&env),
),
];

assert_eq!(env.events().all(), event)
}

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

// Try to add spender without initializing - should panic
client.add_approved_spender(&admin, &spender);
}

#[test]
#[should_panic(expected = "Unauthorized")]
fn test_add_approved_spender_wrong_admin() {
let (env, client) = setup();
let admin = Address::generate(&env);
let wrong_admin = Address::generate(&env);
let token = Address::generate(&env);
let spender = Address::generate(&env);

// Initialize the contract
client.initialize(&admin, &token);

// Try to add spender with wrong admin - should panic
client.add_approved_spender(&wrong_admin, &spender);
}

#[test]
fn test_add_multiple_approved_spenders() {
let (env, client) = setup();
let admin = Address::generate(&env);
let token = Address::generate(&env);
let spender1 = Address::generate(&env);
let spender2 = Address::generate(&env);

// Initialize the contract
client.initialize(&admin, &token);

// Add multiple spenders - should succeed without panic
client.add_approved_spender(&admin, &spender1);
client.add_approved_spender(&admin, &spender2);
}

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

// Initialize the contract
client.initialize(&admin, &token);

// Add same spender twice (should not panic, just overwrite)
client.add_approved_spender(&admin, &spender);
client.add_approved_spender(&admin, &spender);
}
3 changes: 2 additions & 1 deletion contracts/reward-pool/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use soroban_sdk::contracttype;
use soroban_sdk::{contracttype, Address};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DataKey {
Admin,
Token,
Spender(Address),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"generators": {
"address": 3,
"nonce": 0,
"mux_id": 0
},
"auth": [
[],
[]
],
"ledger": {
"protocol_version": 23,
"sequence_number": 0,
"timestamp": 0,
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
"base_reserve": 0,
"min_persistent_entry_ttl": 4096,
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent",
"val": {
"contract_instance": {
"executable": {
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"storage": null
}
}
}
},
"ext": "v0"
},
4095
]
],
[
{
"contract_code": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_code": {
"ext": "v0",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"code": ""
}
},
"ext": "v0"
},
4095
]
]
]
},
"events": []
}
Loading