From f54db1ba02a2329fea7985be81f9e00a93d016b2 Mon Sep 17 00:00:00 2001 From: tekprecious Date: Mon, 28 Sep 2026 00:05:35 +0100 Subject: [PATCH 1/3] feat: implement treasury contract (#994) --- Cargo.toml | 2 +- treasury-contract/Cargo.toml | 14 +++++ treasury-contract/src/lib.rs | 103 ++++++++++++++++++++++++++++++++++ treasury-contract/src/test.rs | 63 +++++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 treasury-contract/Cargo.toml create mode 100644 treasury-contract/src/lib.rs create mode 100644 treasury-contract/src/test.rs diff --git a/Cargo.toml b/Cargo.toml index 1337e989..10f61c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["creator-keys", "creator-keys-factory", "referral-contract"] +members = ["creator-keys", "creator-keys-factory", "referral-contract", "treasury-contract"] resolver = "2" [workspace.package] diff --git a/treasury-contract/Cargo.toml b/treasury-contract/Cargo.toml new file mode 100644 index 00000000..892bfd9d --- /dev/null +++ b/treasury-contract/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "treasury-contract" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/treasury-contract/src/lib.rs b/treasury-contract/src/lib.rs new file mode 100644 index 00000000..66bf5271 --- /dev/null +++ b/treasury-contract/src/lib.rs @@ -0,0 +1,103 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Symbol, Vec}; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + Balance, + Epoch, + EpochHistory(u32), +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EpochDistribution { + pub epoch: u32, + pub recipients: Vec
, + pub amounts: Vec, +} + +#[contract] +pub struct TreasuryContract; + +#[contractimpl] +impl TreasuryContract { + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic!("Already initialized"); + } + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Balance, &0i128); + env.storage().instance().set(&DataKey::Epoch, &0u32); + } + + pub fn collect_fee(env: Env, key_id: String, amount: i128) { + if amount <= 0 { + panic!("Amount must be positive"); + } + + let mut balance: i128 = env.storage().instance().get(&DataKey::Balance).unwrap_or(0); + balance += amount; + env.storage().instance().set(&DataKey::Balance, &balance); + + env.events() + .publish((Symbol::new(&env, "FeeCollected"), key_id), amount); + } + + pub fn get_treasury_balance(env: Env) -> i128 { + env.storage().instance().get(&DataKey::Balance).unwrap_or(0) + } + + pub fn distribute(env: Env, recipients: Vec
, amounts: Vec) { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Not initialized"); + admin.require_auth(); + + if recipients.len() != amounts.len() { + panic!("Recipients and amounts length mismatch"); + } + + let mut total_amount: i128 = 0; + for amount in amounts.iter() { + if amount <= 0 { + panic!("Amount must be positive"); + } + total_amount += amount; + } + + let mut balance: i128 = env.storage().instance().get(&DataKey::Balance).unwrap_or(0); + if total_amount > balance { + panic!("Insufficient balance"); + } + + balance -= total_amount; + env.storage().instance().set(&DataKey::Balance, &balance); + + let epoch: u32 = env.storage().instance().get(&DataKey::Epoch).unwrap_or(0); + let distribution = EpochDistribution { + epoch, + recipients: recipients.clone(), + amounts: amounts.clone(), + }; + + env.storage() + .persistent() + .set(&DataKey::EpochHistory(epoch), &distribution); + env.storage().instance().set(&DataKey::Epoch, &(epoch + 1)); + + env.events() + .publish((Symbol::new(&env, "FeeDistributed"), epoch), distribution); + } + + pub fn get_distribution_history(env: Env, epoch: u32) -> Option { + env.storage().persistent().get(&DataKey::EpochHistory(epoch)) + } +} + +#[cfg(test)] +mod test; diff --git a/treasury-contract/src/test.rs b/treasury-contract/src/test.rs new file mode 100644 index 00000000..e019b51a --- /dev/null +++ b/treasury-contract/src/test.rs @@ -0,0 +1,63 @@ +#![cfg(test)] + +use super::*; +use soroban_sdk::{testutils::Address as _, vec, Address, Env, String}; + +#[test] +fn test_treasury() { + let env = Env::default(); + let contract_id = env.register_contract(None, TreasuryContract); + let client = TreasuryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + let key_id = String::from_str(&env, "key123"); + + // Collect fee + client.collect_fee(&key_id, &1000); + assert_eq!(client.get_treasury_balance(), 1000); + + client.collect_fee(&key_id, &500); + assert_eq!(client.get_treasury_balance(), 1500); + + // Distribute + let recipient1 = Address::generate(&env); + let recipient2 = Address::generate(&env); + let recipients = vec![&env, recipient1.clone(), recipient2.clone()]; + let amounts = vec![&env, 700, 300]; + + // Mock admin auth + env.mock_all_auths(); + + client.distribute(&recipients, &amounts); + + assert_eq!(client.get_treasury_balance(), 500); + + let history = client.get_distribution_history(&0).unwrap(); + assert_eq!(history.epoch, 0); + assert_eq!(history.recipients, recipients); + assert_eq!(history.amounts, amounts); +} + +#[test] +#[should_panic(expected = "Insufficient balance")] +fn test_distribute_insufficient_balance() { + let env = Env::default(); + let contract_id = env.register_contract(None, TreasuryContract); + let client = TreasuryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + let key_id = String::from_str(&env, "key123"); + client.collect_fee(&key_id, &100); + + let recipient = Address::generate(&env); + let recipients = vec![&env, recipient]; + let amounts = vec![&env, 200]; + + env.mock_all_auths(); + + client.distribute(&recipients, &amounts); +} From 790f7d3328e175140ec525164d08e6cdc4dc8bbc Mon Sep 17 00:00:00 2001 From: tekprecious Date: Mon, 28 Sep 2026 00:09:46 +0100 Subject: [PATCH 2/3] fix: resolve rustfmt failure in treasury-contract --- treasury-contract/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/treasury-contract/src/lib.rs b/treasury-contract/src/lib.rs index 66bf5271..4e751765 100644 --- a/treasury-contract/src/lib.rs +++ b/treasury-contract/src/lib.rs @@ -95,7 +95,9 @@ impl TreasuryContract { } pub fn get_distribution_history(env: Env, epoch: u32) -> Option { - env.storage().persistent().get(&DataKey::EpochHistory(epoch)) + env.storage() + .persistent() + .get(&DataKey::EpochHistory(epoch)) } } From b4c3b53a5e3b45f81cb4c27ee24bf85e9f8b4272 Mon Sep 17 00:00:00 2001 From: tekprecious Date: Mon, 28 Sep 2026 00:51:20 +0100 Subject: [PATCH 3/3] fix: resolve clippy deprecated register_contract warning --- treasury-contract/src/test.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/treasury-contract/src/test.rs b/treasury-contract/src/test.rs index e019b51a..4c5882f4 100644 --- a/treasury-contract/src/test.rs +++ b/treasury-contract/src/test.rs @@ -6,7 +6,7 @@ use soroban_sdk::{testutils::Address as _, vec, Address, Env, String}; #[test] fn test_treasury() { let env = Env::default(); - let contract_id = env.register_contract(None, TreasuryContract); + let contract_id = env.register(TreasuryContract, ()); let client = TreasuryContractClient::new(&env, &contract_id); let admin = Address::generate(&env); @@ -44,7 +44,7 @@ fn test_treasury() { #[should_panic(expected = "Insufficient balance")] fn test_distribute_insufficient_balance() { let env = Env::default(); - let contract_id = env.register_contract(None, TreasuryContract); + let contract_id = env.register(TreasuryContract, ()); let client = TreasuryContractClient::new(&env, &contract_id); let admin = Address::generate(&env);