diff --git a/contracts/contracts/group_treasury/src/lib.rs b/contracts/contracts/group_treasury/src/lib.rs index 278b60c..eb84738 100644 --- a/contracts/contracts/group_treasury/src/lib.rs +++ b/contracts/contracts/group_treasury/src/lib.rs @@ -23,18 +23,37 @@ pub struct GroupTreasuryContract; #[contractimpl] impl GroupTreasuryContract { - /// One-time initialisation. Sets the admin and sets up the balances map and members set. - pub fn initialize(env: Env, admin: Address, _token: Address) { + /// One-time initialisation. Sets the admin, the approval `threshold`, and sets up the + /// balances map and members set. `threshold` is the number of approvals required to + /// execute a withdraw proposal and must be at least 1. + pub fn initialize(env: Env, admin: Address, _token: Address, threshold: u32) { if env.storage().instance().has(&DataKey::Admin) { panic!("already initialized"); } + if threshold == 0 { + panic!("threshold must be at least 1"); + } env.storage().instance().set(&DataKey::Admin, &admin); + env.storage() + .instance() + .set(&DataKey::Threshold, &threshold); + env.storage() + .instance() + .set(&DataKey::ProposalCount, &0u32); let balances: Map = Map::new(&env); env.storage().instance().set(&DataKey::Balances, &balances); let members: Vec
= Vec::new(&env); env.storage().instance().set(&DataKey::Members, &members); } + /// Returns the configured approval threshold. + pub fn get_threshold(env: Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::Threshold) + .expect("not initialized") + } + /// Admin-only: Add a new member to the treasury. pub fn add_member(env: Env, member: Address) { let admin = require_admin(&env); diff --git a/contracts/contracts/group_treasury/src/storage.rs b/contracts/contracts/group_treasury/src/storage.rs index bdfc58c..e51a794 100644 --- a/contracts/contracts/group_treasury/src/storage.rs +++ b/contracts/contracts/group_treasury/src/storage.rs @@ -5,6 +5,32 @@ pub enum DataKey { Admin, Balances, Members, + Threshold, // u32: approvals required to execute a withdraw proposal + ProposalCount, // u32: total proposals created (also next id source) + Proposal(u32), // WithdrawProposal by id +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ProposalStatus { + Active, + Passed, + Rejected, + Executed, +} + +#[contracttype] +#[derive(Clone)] +pub struct WithdrawProposal { + pub id: u32, + pub proposer: Address, + pub to: Address, + pub token: Address, + pub amount: i128, + pub approvals: u32, + pub rejections: u32, + pub status: ProposalStatus, + pub expires_at: u64, } #[contracttype] diff --git a/contracts/contracts/group_treasury/src/test.rs b/contracts/contracts/group_treasury/src/test.rs index 9da316f..7817cd5 100644 --- a/contracts/contracts/group_treasury/src/test.rs +++ b/contracts/contracts/group_treasury/src/test.rs @@ -59,7 +59,7 @@ fn setup(env: &Env) -> (Address, Address, Address, Address) { let contract_id = env.register(GroupTreasuryContract, ()); let client = GroupTreasuryContractClient::new(env, &contract_id); - client.initialize(&admin, &token_id); + client.initialize(&admin, &token_id, &1); (contract_id, token_id, admin, member) } @@ -79,7 +79,7 @@ fn test_double_initialize_panics() { let (contract_id, token_id, _admin, _member) = setup(&env); let client = GroupTreasuryContractClient::new(&env, &contract_id); let other = Address::generate(&env); - client.initialize(&other, &token_id); + client.initialize(&other, &token_id, &1); } #[test] @@ -160,7 +160,7 @@ fn test_non_admin_cannot_withdraw() { let contract_id = env.register(GroupTreasuryContract, ()); let client = GroupTreasuryContractClient::new(&env, &contract_id); - client.initialize(&admin, &token_id); + client.initialize(&admin, &token_id, &1); let recipient = Address::generate(&env); // admin.require_auth() inside withdraw will fail — no auth context set up. @@ -197,7 +197,7 @@ fn test_multi_token_deposits_tracked_separately() { let contract_id = env.register(GroupTreasuryContract, ()); let client = GroupTreasuryContractClient::new(&env, &contract_id); - client.initialize(&admin, &xlm_id); // initialize with XLM for compatibility + client.initialize(&admin, &xlm_id, &1); // initialize with XLM for compatibility // Deposit XLM and USDC client.deposit(&member, &xlm_id, &40_000); @@ -262,7 +262,7 @@ fn test_non_admin_cannot_add_member() { let token_id = env.register(mock_token::MockToken, ()); let contract_id = env.register(GroupTreasuryContract, ()); let client = GroupTreasuryContractClient::new(&env, &contract_id); - client.initialize(&admin, &token_id); + client.initialize(&admin, &token_id, &1); // non_admin tries to add member - should fail due to auth client.add_member(&member); @@ -335,3 +335,32 @@ fn test_initialize_creates_empty_members_list() { let members = client.get_members(); assert_eq!(members.len(), 0); } + +// ── Threshold Tests ─────────────────────────────────────────────────────────── + +#[test] +fn test_get_threshold_returns_configured_value() { + let env = Env::default(); + + let admin = Address::generate(&env); + let token_id = env.register(mock_token::MockToken, ()); + + let contract_id = env.register(GroupTreasuryContract, ()); + let client = GroupTreasuryContractClient::new(&env, &contract_id); + client.initialize(&admin, &token_id, &3); + + assert_eq!(client.get_threshold(), 3); +} + +#[test] +#[should_panic(expected = "threshold must be at least 1")] +fn test_initialize_zero_threshold_panics() { + let env = Env::default(); + + let admin = Address::generate(&env); + let token_id = env.register(mock_token::MockToken, ()); + + let contract_id = env.register(GroupTreasuryContract, ()); + let client = GroupTreasuryContractClient::new(&env, &contract_id); + client.initialize(&admin, &token_id, &0); +}