Skip to content
Open
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
23 changes: 21 additions & 2 deletions contracts/contracts/group_treasury/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address, i128> = Map::new(&env);
env.storage().instance().set(&DataKey::Balances, &balances);
let members: Vec<Address> = 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);
Expand Down
26 changes: 26 additions & 0 deletions contracts/contracts/group_treasury/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
39 changes: 34 additions & 5 deletions contracts/contracts/group_treasury/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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]
Expand Down Expand Up @@ -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.
Expand Down Expand 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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}