#[cfg(test)]
mod tests {
use crate::common_setup::{contract_boxes_empty::contract_group, helpers::member};
use crate::common_setup::{
contract_boxes_empty::contract_splits, helpers::mock_app_builder_init_funds,
};
use cosmwasm_std::{to_json_binary, Addr, Coin};
use cw2::{query_contract_info, ContractVersion};
use cw4::{Cw4ExecuteMsg, Member, MemberListResponse};
use cw4_group::msg::ExecuteMsg as Cw4GroupExecuteMsg;
use cw_multi_test::{next_block, App, Executor as TestExecutor};
use sg_controllers::ContractInstantiateMsg;
use sg_splits::contract::MAX_GROUP_SIZE;
use sg_splits::msg::Group;
use sg_splits::{
msg::{InstantiateMsg, QueryMsg},
ContractError,
};
const OWNER: &str = "admin0001";
const MEMBER1: &str = "member0001";
const MEMBER2: &str = "member0002";
const MEMBER3: &str = "member0003";
fn instantiate_group(app: &mut App, members: Vec<Member>) -> Addr {
let group_id = app.store_code(contract_group());
let msg = cw4_group::msg::InstantiateMsg {
admin: Some(OWNER.into()),
members,
};
app.instantiate_contract(group_id, Addr::unchecked(OWNER), &msg, &[], "group", None)
.unwrap()
}
#[track_caller]
fn instantiate_splits_with_group(app: &mut App, group_addr: Addr) -> Addr {
let flex_id = app.store_code(contract_splits());
let msg = sg_splits::msg::InstantiateMsg {
group: Group::Cw4Address(group_addr.to_string()),
admin: None,
};
app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
.unwrap()
}
#[track_caller]
fn instantiate_splits(app: &mut App) -> Addr {
let flex_id = app.store_code(contract_splits());
let group_msg = cw4_group::msg::InstantiateMsg {
admin: Some(OWNER.into()),
members: vec![
member(OWNER, 50),
member(MEMBER1, 25),
member(MEMBER2, 20),
member(MEMBER3, 5),
],
};
let msg = sg_splits::msg::InstantiateMsg {
group: Group::Cw4Instantiate(ContractInstantiateMsg {
code_id: app.store_code(contract_group()),
msg: to_json_binary(&group_msg).unwrap(),
admin: None,
label: "cw4-group".to_string(),
}),
admin: Some(OWNER.into()),
};
app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
.unwrap()
}
#[track_caller]
fn instantiate_splits_with_overflow_group(app: &mut App) -> Addr {
let flex_id = app.store_code(contract_splits());
let members: Vec<Member> = (1..=MAX_GROUP_SIZE + 1)
.map(|i| member(format!("member{i:04}"), 1))
.collect();
let group_msg = cw4_group::msg::InstantiateMsg {
admin: Some(OWNER.into()),
members,
};
let msg = sg_splits::msg::InstantiateMsg {
group: Group::Cw4Instantiate(ContractInstantiateMsg {
code_id: app.store_code(contract_group()),
msg: to_json_binary(&group_msg).unwrap(),
admin: None,
label: "cw4-group".to_string(),
}),
admin: Some(OWNER.into()),
};
app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
.unwrap()
}
#[track_caller]
fn setup_test_case(
app: &mut App,
init_funds: Vec<Coin>,
multisig_as_group_admin: bool,
) -> (Addr, Addr) {
let members = vec![
member(OWNER, 50),
member(MEMBER1, 25),
member(MEMBER2, 20),
member(MEMBER3, 5),
];
let group_addr = instantiate_group(app, members);
app.update_block(next_block);
let splits_addr = instantiate_splits_with_group(app, group_addr.clone());
app.update_block(next_block);
if multisig_as_group_admin {
let update_admin = Cw4ExecuteMsg::UpdateAdmin {
admin: Some(splits_addr.to_string()),
};
app.execute_contract(
Addr::unchecked(OWNER),
group_addr.clone(),
&update_admin,
&[],
)
.unwrap();
app.update_block(next_block);
}
if !init_funds.is_empty() {
app.send_tokens(Addr::unchecked(OWNER), splits_addr.clone(), &init_funds)
.unwrap();
}
(splits_addr, group_addr)
}
#[track_caller]
fn setup_test_case_with_internal_group(app: &mut App, init_funds: Vec<Coin>) -> (Addr, Addr) {
let splits_addr = instantiate_splits(app);
app.update_block(next_block);
if !init_funds.is_empty() {
app.send_tokens(Addr::unchecked(OWNER), splits_addr.clone(), &init_funds)
.unwrap();
}
let group_addr: Addr = app
.wrap()
.query_wasm_smart(&splits_addr, &QueryMsg::Group {})
.unwrap();
(splits_addr, group_addr)
}
#[track_caller]
fn setup_test_case_with_overflow_group(app: &mut App, init_funds: Vec<Coin>) -> (Addr, Addr) {
let splits_addr = instantiate_splits_with_overflow_group(app);
app.update_block(next_block);
if !init_funds.is_empty() {
app.send_tokens(Addr::unchecked(OWNER), splits_addr.clone(), &init_funds)
.unwrap();
}
let group_addr: Addr = app
.wrap()
.query_wasm_smart(&splits_addr, &QueryMsg::Group {})
.unwrap();
(splits_addr, group_addr)
}
#[test]
fn test_instantiate_works() {
let mut app = mock_app_builder_init_funds(&[]);
let splits_id = app.store_code(contract_splits());
let group_addr = instantiate_group(&mut app, vec![member(OWNER, 0)]);
let instantiate_msg = InstantiateMsg {
group: Group::Cw4Address(group_addr.to_string()),
admin: None,
};
let err = app
.instantiate_contract(
splits_id,
Addr::unchecked(OWNER),
&instantiate_msg,
&[],
"greater than zero required total weight",
None,
)
.unwrap_err();
assert_eq!(
ContractError::InvalidWeight { weight: 0 },
err.downcast().unwrap()
);
let group_addr = instantiate_group(&mut app, vec![member(OWNER, 1)]);
let instantiate_msg = InstantiateMsg {
group: Group::Cw4Address(group_addr.to_string()),
admin: None,
};
let splits_addr = app
.instantiate_contract(
splits_id,
Addr::unchecked(OWNER),
&instantiate_msg,
&[],
"single member group with weight is valid",
None,
)
.unwrap();
let version = query_contract_info(&app.wrap(), splits_addr.clone()).unwrap();
assert_eq!(
ContractVersion {
contract: "crates.io:sg-splits".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
version,
);
let members: MemberListResponse = app
.wrap()
.query_wasm_smart(
&splits_addr,
&QueryMsg::ListMembers {
start_after: None,
limit: None,
},
)
.unwrap();
assert_eq!(
members.members,
vec![Member {
addr: OWNER.into(),
weight: 1
}]
);
}
mod distribute {
use cosmwasm_std::{coins, Uint128};
use cw4::Cw4Contract;
use cw_multi_test::{BankSudo, SudoMsg};
use super::*;
use sg_splits::msg::{ExecuteMsg, QueryMsg};
#[test]
fn distribute_zero_funds() {
let mut app = mock_app_builder_init_funds(&[]);
let (splits_addr, _) = setup_test_case(&mut app, vec![], false);
let msg = ExecuteMsg::Distribute { denom_list: None };
let err = app
.execute_contract(Addr::unchecked(OWNER), splits_addr, &msg, &[])
.unwrap_err();
assert_eq!(ContractError::NoFunds {}, err.downcast().unwrap());
}
#[test]
fn distribute_non_member() {
const DENOM: &str = "ustars";
let init_funds = coins(100, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, _) = setup_test_case(&mut app, init_funds, false);
let msg = ExecuteMsg::Distribute { denom_list: None };
app.execute_contract(
Addr::unchecked("non_member".to_string()),
splits_addr,
&msg,
&[],
)
.unwrap_err();
}
#[test]
fn distribute() {
const DENOM: &str = "ustars";
let init_funds = coins(100, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, _) = setup_test_case_with_internal_group(&mut app, init_funds);
let msg = ExecuteMsg::Distribute { denom_list: None };
app.execute_contract(Addr::unchecked(OWNER), splits_addr.clone(), &msg, &[])
.unwrap();
let bal = app.wrap().query_all_balances(splits_addr.clone()).unwrap();
assert_eq!(bal, &[]);
let msg = QueryMsg::ListMembers {
start_after: None,
limit: None,
};
let list: MemberListResponse = app.wrap().query_wasm_smart(splits_addr, &msg).unwrap();
let mut expected_balances = vec![
Uint128::new(5),
Uint128::new(20),
Uint128::new(25),
Uint128::new(50),
];
for member in list.members.iter() {
let bal = app
.wrap()
.query_balance(member.addr.to_string(), DENOM)
.unwrap();
assert_eq!(bal.amount, expected_balances.pop().unwrap())
}
}
#[test]
fn distribute_under_funded() {
const DENOM: &str = "ustars";
let init_funds = coins(79, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, group_addr) =
setup_test_case_with_internal_group(&mut app, init_funds);
let total_weight = Cw4Contract(group_addr).total_weight(&app.wrap()).unwrap();
let msg = ExecuteMsg::Distribute { denom_list: None };
let err = app
.execute_contract(Addr::unchecked(OWNER), splits_addr, &msg, &[])
.unwrap_err();
assert_eq!(
err.source().unwrap().to_string(),
ContractError::NotEnoughFunds { min: total_weight }.to_string()
);
}
#[test]
fn distribute_amount_with_remaining_balance() {
const DENOM: &str = "ustars";
let init_funds = coins(479, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, group_addr) =
setup_test_case_with_internal_group(&mut app, init_funds.clone());
let total_weight = Cw4Contract(group_addr).total_weight(&app.wrap()).unwrap();
let multiplier = init_funds[0].amount / Uint128::from(total_weight);
let contract_balance = init_funds[0].amount - multiplier * Uint128::from(total_weight);
let msg = ExecuteMsg::Distribute { denom_list: None };
let _ = app
.execute_contract(Addr::unchecked(OWNER), splits_addr.clone(), &msg, &[])
.unwrap();
let bal = app.wrap().query_all_balances(splits_addr.clone()).unwrap();
assert_eq!(bal, coins(contract_balance.u128(), DENOM));
let msg = QueryMsg::ListMembers {
start_after: None,
limit: None,
};
let list: MemberListResponse = app.wrap().query_wasm_smart(splits_addr, &msg).unwrap();
for member in list.members.iter() {
let bal = app
.wrap()
.query_balance(member.addr.to_string(), DENOM)
.unwrap();
assert_eq!(bal.amount, Uint128::from(member.weight) * multiplier)
}
}
#[test]
fn distribute_with_too_many_members() {
const DENOM: &str = "ustars";
let init_funds = coins(255, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, _) = setup_test_case_with_overflow_group(&mut app, init_funds);
let msg = ExecuteMsg::Distribute { denom_list: None };
let err = app
.execute_contract(Addr::unchecked(OWNER), splits_addr, &msg, &[])
.unwrap_err();
assert_eq!(
err.source().unwrap().to_string(),
ContractError::InvalidMemberCount {
count: (MAX_GROUP_SIZE + 1) as usize
}
.to_string()
);
}
#[test]
fn distribute_with_zero_weight_members() {
const DENOM: &str = "ustars";
let init_funds = coins(255, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, group_addr) =
setup_test_case_with_internal_group(&mut app, init_funds);
let msg = Cw4GroupExecuteMsg::UpdateMembers {
remove: vec![],
add: vec![member("member0100", 0), member("member0101", 0)],
};
let _ = app
.execute_contract(Addr::unchecked(OWNER), group_addr, &msg, &[])
.unwrap();
let msg = ExecuteMsg::Distribute { denom_list: None };
let _ = app
.execute_contract(Addr::unchecked(OWNER), splits_addr, &msg, &[])
.unwrap();
let bal = app
.wrap()
.query_balance("memeber0100".to_string(), DENOM)
.unwrap();
assert_eq!(bal.amount, Uint128::zero());
let bal = app
.wrap()
.query_balance("memeber0101".to_string(), DENOM)
.unwrap();
assert_eq!(bal.amount, Uint128::zero());
}
#[test]
fn distribute_with_group_changes() {
const DENOM: &str = "ustars";
let init_funds = coins(199, DENOM);
let mut app = mock_app_builder_init_funds(&init_funds);
let (splits_addr, group_addr) = setup_test_case(&mut app, init_funds.clone(), false);
let total_weight = Cw4Contract(group_addr.clone())
.total_weight(&app.wrap())
.unwrap();
let multiplier = init_funds[0].amount / Uint128::from(total_weight);
let contract_balance = init_funds[0].amount - multiplier * Uint128::from(total_weight);
let mut payouts = vec![];
let msg = ExecuteMsg::Distribute { denom_list: None };
let _ = app
.execute_contract(Addr::unchecked(OWNER), splits_addr.clone(), &msg, &[])
.unwrap();
let bal = app.wrap().query_all_balances(splits_addr.clone()).unwrap();
assert_eq!(bal, coins(contract_balance.u128(), DENOM));
let msg = QueryMsg::ListMembers {
start_after: None,
limit: None,
};
let list: MemberListResponse = app
.wrap()
.query_wasm_smart(splits_addr.clone(), &msg)
.unwrap();
for member in list.members.iter() {
let bal = app
.wrap()
.query_balance(member.addr.to_string(), DENOM)
.unwrap();
payouts.push(bal.amount);
assert_eq!(bal.amount, Uint128::from(member.weight) * multiplier)
}
let msg = Cw4GroupExecuteMsg::UpdateMembers {
remove: vec![],
add: vec![member("member0100", 2), member("member0101", 23)],
};
let _ = app
.execute_contract(Addr::unchecked(OWNER), group_addr.clone(), &msg, &[])
.unwrap();
payouts.push(Uint128::zero());
payouts.push(Uint128::zero());
let member = Cw4Contract(group_addr.clone())
.is_member(&app.wrap(), &Addr::unchecked("member0100"), None)
.unwrap();
assert!(member.is_some());
let member = Cw4Contract(group_addr.clone())
.is_member(&app.wrap(), &Addr::unchecked("member0101"), None)
.unwrap();
assert!(member.is_some());
let more_funds = coins(12345u128, DENOM);
app.sudo(SudoMsg::Bank({
BankSudo::Mint {
to_address: splits_addr.to_string(),
amount: more_funds.clone(),
}
}))
.map_err(|err| println!("{err:?}"))
.ok();
let bal = app.wrap().query_all_balances(splits_addr.clone()).unwrap();
assert_eq!(
bal,
coins(contract_balance.u128() + more_funds[0].amount.u128(), DENOM)
);
let msg = ExecuteMsg::Distribute { denom_list: None };
let _ = app
.execute_contract(Addr::unchecked(OWNER), splits_addr.clone(), &msg, &[])
.unwrap();
let new_total_weight = Cw4Contract(group_addr).total_weight(&app.wrap()).unwrap();
let new_multiplier =
(contract_balance + more_funds[0].amount) / Uint128::from(new_total_weight);
let new_contract_balance = (contract_balance + more_funds[0].amount)
- new_multiplier * Uint128::from(new_total_weight);
let bal = app.wrap().query_all_balances(splits_addr.clone()).unwrap();
assert_eq!(bal, coins(new_contract_balance.u128(), DENOM));
let msg = QueryMsg::ListMembers {
start_after: None,
limit: None,
};
let list: MemberListResponse = app.wrap().query_wasm_smart(splits_addr, &msg).unwrap();
for (i, member) in list.members.iter().enumerate() {
let bal = app
.wrap()
.query_balance(member.addr.to_string(), DENOM)
.unwrap();
assert_eq!(
bal.amount,
payouts[i] + Uint128::from(member.weight) * new_multiplier
)
}
}
}
}