1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4 coins, ensure, to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
5 MessageInfo, Reply, Response, StdResult, SubMsg, Uint128,
6};
7use cw2::set_contract_version;
8use cw4::{Cw4Contract, Member, MemberListResponse, MemberResponse};
9use cw_utils::{maybe_addr, parse_reply_instantiate_data};
10
11use crate::error::ContractError;
12use crate::msg::{ExecuteMsg, Group, InstantiateMsg, QueryMsg};
13use crate::state::{ADMIN, GROUP};
14
15pub const CONTRACT_NAME: &str = "crates.io:sg-splits";
17pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
18
19const INIT_GROUP_REPLY_ID: u64 = 1;
20
21pub const PAGINATION_LIMIT: u32 = 30;
23pub const MAX_GROUP_SIZE: u32 = 25;
25
26#[cfg_attr(not(feature = "library"), entry_point)]
27pub fn instantiate(
28 mut deps: DepsMut,
29 env: Env,
30 _info: MessageInfo,
31 msg: InstantiateMsg,
32) -> Result<Response, ContractError> {
33 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
34
35 let self_addr = env.contract.address;
36
37 let admin_addr = maybe_addr(deps.api, msg.admin)?;
38 ADMIN.set(deps.branch(), admin_addr)?;
39
40 match msg.group {
41 Group::Cw4Instantiate(init) => Ok(Response::default().add_submessage(
42 SubMsg::reply_on_success(init.into_wasm_msg(self_addr), INIT_GROUP_REPLY_ID),
43 )),
44 Group::Cw4Address(addr) => {
45 let group = Cw4Contract(
46 deps.api
47 .addr_validate(&addr)
48 .map_err(|_| ContractError::InvalidGroup { addr })?,
49 );
50
51 checked_total_weight(&group, deps.as_ref())?;
52 checked_total_members(&group, deps.as_ref())?;
53
54 GROUP.save(deps.storage, &group)?;
55 Ok(Response::default())
56 }
57 }
58}
59
60#[cfg_attr(not(feature = "library"), entry_point)]
61pub fn execute(
62 deps: DepsMut,
63 env: Env,
64 info: MessageInfo,
65 msg: ExecuteMsg,
66) -> Result<Response, ContractError> {
67 let api = deps.api;
68
69 match msg {
70 ExecuteMsg::UpdateAdmin { admin } => {
71 Ok(ADMIN.execute_update_admin(deps, info, maybe_addr(api, admin)?)?)
72 }
73 ExecuteMsg::Distribute { denom_list } => {
74 execute_distribute(deps.as_ref(), env, info, denom_list)
75 }
76 }
77}
78
79pub fn execute_distribute(
80 deps: Deps,
81 env: Env,
82 info: MessageInfo,
83 denom_list: Option<Vec<String>>,
84) -> Result<Response, ContractError> {
85 if !can_distribute(deps, info)? {
86 return Err(ContractError::Unauthorized {});
87 }
88
89 let group = GROUP.load(deps.storage)?;
90
91 let total_weight = checked_total_weight(&group, deps)?;
92 let members = group.list_members(&deps.querier, None, Some(PAGINATION_LIMIT))?;
93 let members_count = members.len();
94 if members_count == 0 || members_count > MAX_GROUP_SIZE as usize {
95 return Err(ContractError::InvalidMemberCount {
96 count: members_count,
97 });
98 }
99 let mut funds: Vec<Coin> = Vec::new();
100 if let Some(denom_list) = denom_list {
101 for denom in denom_list.iter() {
102 let balance = deps
103 .querier
104 .query_balance(env.contract.address.clone(), denom)?;
105 if balance.amount.is_zero() {
106 continue;
107 }
108 funds.push(balance);
109 }
110 } else {
111 funds = deps.querier.query_all_balances(env.contract.address)?;
112 }
113
114 ensure!(!funds.is_empty(), ContractError::NoFunds {});
115
116 let mut msgs: Vec<CosmosMsg> = Vec::new();
117 for member in members.iter().filter(|m| m.weight > 0) {
118 for coin in funds.iter() {
119 let multiplier = coin.amount / Uint128::from(total_weight);
122 if multiplier.is_zero() {
123 continue;
124 }
125
126 let amount = Uint128::from(member.weight) * multiplier;
127 msgs.push(CosmosMsg::Bank(BankMsg::Send {
128 to_address: member.addr.clone(),
129 amount: coins(amount.u128(), coin.denom.clone()),
130 }));
131 }
132 }
133
134 ensure!(
135 !msgs.is_empty(),
136 ContractError::NotEnoughFunds { min: total_weight }
137 );
138
139 Ok(Response::new()
140 .add_attribute("action", "distribute")
141 .add_messages(msgs))
142}
143
144fn checked_total_weight(group: &Cw4Contract, deps: Deps) -> Result<u64, ContractError> {
145 let weight = group.total_weight(&deps.querier)?;
146 if weight == 0 {
147 return Err(ContractError::InvalidWeight { weight });
148 }
149
150 Ok(weight)
151}
152
153fn checked_total_members(group: &Cw4Contract, deps: Deps) -> Result<u64, ContractError> {
154 let members = group
155 .list_members(&deps.querier, None, Some(PAGINATION_LIMIT))?
156 .len();
157 if members == 0 || members > MAX_GROUP_SIZE as usize {
158 return Err(ContractError::InvalidMemberCount { count: members });
159 }
160
161 Ok(members as u64)
162}
163
164fn can_distribute(deps: Deps, info: MessageInfo) -> StdResult<bool> {
166 match ADMIN.get(deps)? {
167 Some(admin) => Ok(admin == info.sender),
168 None => Ok(GROUP
169 .load(deps.storage)?
170 .is_member(&deps.querier, &info.sender, None)?
171 .is_some()),
172 }
173}
174
175#[cfg_attr(not(feature = "library"), entry_point)]
176pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
177 match msg {
178 QueryMsg::Admin {} => to_json_binary(&ADMIN.query_admin(deps)?),
179 QueryMsg::Group {} => to_json_binary(&query_group(deps)?),
180 QueryMsg::ListMembers { start_after, limit } => {
181 to_json_binary(&list_members(deps, start_after, limit)?)
182 }
183 QueryMsg::Member { address } => to_json_binary(&query_member(deps, address)?),
184 }
185}
186
187fn query_group(deps: Deps) -> StdResult<Addr> {
188 Ok(GROUP.load(deps.storage)?.addr())
189}
190
191fn query_member(deps: Deps, member: String) -> StdResult<MemberResponse> {
192 let group = GROUP.load(deps.storage)?;
193 let voter_addr = deps.api.addr_validate(&member)?;
194 let weight = group.is_member(&deps.querier, &voter_addr, None)?;
195
196 Ok(MemberResponse { weight })
197}
198
199fn list_members(
200 deps: Deps,
201 start_after: Option<String>,
202 limit: Option<u32>,
203) -> StdResult<MemberListResponse> {
204 let group = GROUP.load(deps.storage)?;
205 let members = group
206 .list_members(&deps.querier, start_after, limit)?
207 .into_iter()
208 .map(|member| Member {
209 addr: member.addr,
210 weight: member.weight,
211 })
212 .collect();
213 Ok(MemberListResponse { members })
214}
215
216#[cfg_attr(not(feature = "library"), entry_point)]
217pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
218 if msg.id != INIT_GROUP_REPLY_ID {
219 return Err(ContractError::InvalidReplyID {});
220 }
221
222 let reply = parse_reply_instantiate_data(msg);
223 match reply {
224 Ok(res) => {
225 let group =
226 Cw4Contract(deps.api.addr_validate(&res.contract_address).map_err(|_| {
227 ContractError::InvalidGroup {
228 addr: res.contract_address.clone(),
229 }
230 })?);
231
232 GROUP.save(deps.storage, &group)?;
233
234 Ok(Response::default().add_attribute("action", "reply_on_success"))
235 }
236 Err(_) => Err(ContractError::ReplyOnSuccess {}),
237 }
238}