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
3 changes: 3 additions & 0 deletions contracts/dividend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
83 changes: 82 additions & 1 deletion contracts/dividend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![no_std]

use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec,
contract, contractimpl, contracttype, token, Address, Env, String, Symbol, Vec,
};

#[contracttype]
Expand Down Expand Up @@ -41,6 +41,18 @@ impl DividendContract {
.set(&DataKey::Distributions, &Vec::<Distribution>::new(&env));
}

/// Transfer admin rights to a new address.
pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) {
current_admin.require_auth();
Self::require_admin(&env, &current_admin);

env.storage().instance().set(&DataKey::Admin, &new_admin);
env.events().publish(
(Symbol::new(&env, "admin_transferred"),),
(current_admin, new_admin),
);
}

/// Distribute profit proportionally based on each member's share weight.
///
/// `recipients` and `shares` must be equal length.
Expand Down Expand Up @@ -120,3 +132,72 @@ impl DividendContract {
if admin != *caller { panic!("unauthorized"); }
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{token::StellarAssetClient, Env, String, Vec};

fn setup() -> (Env, DividendContractClient<'static>, Address) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, DividendContract);
let client = DividendContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let asset = env.register_stellar_asset_contract_v2(token_admin);
let asset_address = asset.address();

StellarAssetClient::new(&env, &asset_address).mint(&contract_id, &1_000_0000000i128);
client.initialize(&admin, &asset_address, &treasury);

(env, client, admin)
}

fn single_recipient_distribution(env: &Env) -> (Vec<Address>, Vec<i128>) {
let recipient = Address::generate(env);
let mut recipients = Vec::new(env);
let mut shares = Vec::new(env);
recipients.push_back(recipient);
shares.push_back(1i128);
(recipients, shares)
}

#[test]
fn test_transfer_admin_allows_new_admin_to_distribute() {
let (env, client, admin) = setup();
let new_admin = Address::generate(&env);
let (recipients, shares) = single_recipient_distribution(&env);

client.transfer_admin(&admin, &new_admin);
let distribution_id = client.distribute(
&new_admin,
&recipients,
&shares,
&100_0000000i128,
&String::from_str(&env, "2026-07"),
);

assert_eq!(distribution_id, 1);
assert_eq!(client.get_distributions().len(), 1);
}

#[test]
#[should_panic]
fn test_old_admin_cannot_call_admin_only_after_transfer() {
let (env, client, admin) = setup();
let new_admin = Address::generate(&env);
let (recipients, shares) = single_recipient_distribution(&env);
client.transfer_admin(&admin, &new_admin);

client.distribute(
&admin,
&recipients,
&shares,
&100_0000000i128,
&String::from_str(&env, "2026-07"),
);
}
}
3 changes: 3 additions & 0 deletions contracts/governance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
68 changes: 68 additions & 0 deletions contracts/governance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ impl GovernanceContract {
env.storage().instance().set(&DataKey::Rules, &rules);
}

pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) {
current_admin.require_auth();
Self::require_admin(&env, &current_admin);

env.storage().instance().set(&DataKey::Admin, &new_admin);
env.events().publish(
(Symbol::new(&env, "admin_transferred"),),
(current_admin, new_admin),
);
}

pub fn update_rules(env: Env, admin: Address, rules: CoopRules) {
admin.require_auth();
Self::require_admin(&env, &admin);
Expand All @@ -73,3 +84,60 @@ impl GovernanceContract {
if admin != *caller { panic!("unauthorized"); }
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::Env;

fn setup() -> (Env, GovernanceContractClient<'static>, Address) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let voting = Address::generate(&env);
let loan = Address::generate(&env);
let treasury = Address::generate(&env);
client.initialize(&admin, &voting, &loan, &treasury);

(env, client, admin)
}

fn replacement_rules() -> CoopRules {
CoopRules {
min_contribution: 20_0000000i128,
contribution_period_days: 14,
max_loan_multiplier: 2,
loan_interest_bps: 300,
voting_quorum: 2,
voting_period_days: 5,
late_penalty_bps: 100,
}
}

#[test]
fn test_transfer_admin_allows_new_admin_to_update_rules() {
let (env, client, admin) = setup();
let new_admin = Address::generate(&env);
let rules = replacement_rules();

client.transfer_admin(&admin, &new_admin);
client.update_rules(&new_admin, &rules);

let stored = client.get_rules();
assert_eq!(stored.min_contribution, rules.min_contribution);
assert_eq!(stored.voting_period_days, rules.voting_period_days);
}

#[test]
#[should_panic]
fn test_old_admin_cannot_call_admin_only_after_transfer() {
let (env, client, admin) = setup();
let new_admin = Address::generate(&env);
client.transfer_admin(&admin, &new_admin);

client.update_rules(&admin, &replacement_rules());
}
}
70 changes: 70 additions & 0 deletions contracts/loan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ impl LoanContract {
env.storage().instance().set(&DataKey::Loans, &Vec::<Loan>::new(&env));
}

/// Transfer admin rights to a new address.
pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) {
current_admin.require_auth();
Self::require_admin(&env, &current_admin);

env.storage().instance().set(&DataKey::Admin, &new_admin);
env.events().publish(
(Symbol::new(&env, "admin_transferred"),),
(current_admin, new_admin),
);
}

/// Member submits a loan request.
pub fn request_loan(
env: Env,
Expand Down Expand Up @@ -201,3 +213,61 @@ impl LoanContract {
panic!("loan not found");
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{token::StellarAssetClient, Env, String};

fn setup() -> (Env, LoanContractClient<'static>, Address, Address) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, LoanContract);
let client = LoanContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let borrower = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let asset = env.register_stellar_asset_contract_v2(token_admin);
let asset_address = asset.address();

StellarAssetClient::new(&env, &asset_address).mint(&contract_id, &1_000_0000000i128);
client.initialize(&admin, &treasury, &asset_address);

(env, client, admin, borrower)
}

#[test]
fn test_transfer_admin_allows_new_admin_to_approve_loan() {
let (env, client, admin, borrower) = setup();
let new_admin = Address::generate(&env);
client.transfer_admin(&admin, &new_admin);

let loan_id = client.request_loan(
&borrower,
&100_0000000i128,
&String::from_str(&env, "Working capital"),
&30,
);
client.approve_loan(&new_admin, &loan_id);

assert_eq!(client.get_loan(&loan_id).status, LoanStatus::Approved);
}

#[test]
#[should_panic]
fn test_old_admin_cannot_call_admin_only_after_transfer() {
let (env, client, admin, borrower) = setup();
let new_admin = Address::generate(&env);
client.transfer_admin(&admin, &new_admin);

let loan_id = client.request_loan(
&borrower,
&100_0000000i128,
&String::from_str(&env, "Working capital"),
&30,
);
client.approve_loan(&admin, &loan_id);
}
}
35 changes: 35 additions & 0 deletions contracts/treasury/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ impl TreasuryContract {
}
}

/// Transfer admin rights to a new address.
pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) {
current_admin.require_auth();
Self::require_admin(&env, &current_admin);

env.storage().instance().set(&DataKey::Admin, &new_admin);
env.events().publish(
(Symbol::new(&env, "admin_transferred"),),
(current_admin, new_admin),
);
}

/// Add a new member to the cooperative.
pub fn add_member(env: Env, admin: Address, member: Address) {
admin.require_auth();
Expand Down Expand Up @@ -354,6 +366,29 @@ mod tests {

// ── contribute edge cases ────────────────────────────────────────────────

#[test]
fn test_transfer_admin_updates_admin() {
let (env, client, admin, _, asset) = setup();
let new_admin = Address::generate(&env);
client.initialize(&admin, &String::from_str(&env, "Test Coop"), &asset);

client.transfer_admin(&admin, &new_admin);

assert_eq!(client.get_info().admin, new_admin);
}

#[test]
#[should_panic]
fn test_old_admin_cannot_call_admin_only_after_transfer() {
let (env, client, admin, _, asset) = setup();
let new_admin = Address::generate(&env);
let member = Address::generate(&env);
client.initialize(&admin, &String::from_str(&env, "Test Coop"), &asset);
client.transfer_admin(&admin, &new_admin);

client.add_member(&admin, &member);
}

#[test]
#[should_panic]
fn test_contribute_zero_amount() {
Expand Down
57 changes: 57 additions & 0 deletions contracts/voting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ impl VotingContract {
env.storage().instance().set(&DataKey::Proposals, &Vec::<Proposal>::new(&env));
}

/// Transfer admin rights to a new address.
pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) {
current_admin.require_auth();
Self::require_admin(&env, &current_admin);

env.storage().instance().set(&DataKey::Admin, &new_admin);
env.events().publish(
(Symbol::new(&env, "admin_transferred"),),
(current_admin, new_admin),
);
}

/// Create a new governance proposal.
pub fn create_proposal(
env: Env,
Expand Down Expand Up @@ -204,10 +216,55 @@ impl VotingContract {
.unwrap_or(Map::new(&env))
}

fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
}

fn find_proposal_idx(proposals: &Vec<Proposal>, id: u32) -> u32 {
for i in 0..proposals.len() {
if proposals.get(i).unwrap().id == id { return i; }
}
panic!("proposal not found");
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::Env;

fn setup() -> (Env, VotingContractClient<'static>, Address) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, VotingContract);
let client = VotingContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let treasury = Address::generate(&env);
client.initialize(&admin, &treasury);

(env, client, admin)
}

#[test]
fn test_transfer_admin_allows_new_admin_to_transfer_again() {
let (env, client, admin) = setup();
let new_admin = Address::generate(&env);
let final_admin = Address::generate(&env);

client.transfer_admin(&admin, &new_admin);
client.transfer_admin(&new_admin, &final_admin);
}

#[test]
#[should_panic]
fn test_old_admin_cannot_call_admin_only_after_transfer() {
let (env, client, admin) = setup();
let new_admin = Address::generate(&env);
let final_admin = Address::generate(&env);
client.transfer_admin(&admin, &new_admin);

client.transfer_admin(&admin, &final_admin);
}
}
Loading