sg_splits/
contract.rs

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
15// Version info for migration info
16pub 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
21// This is the same hardcoded value as in cw4-group
22pub const PAGINATION_LIMIT: u32 = 30;
23// We hardcode a smaller number to effectively check group size
24pub 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            // To avoid rounding errors, distribute funds modulo the total weight.
120            // Keep remaining balance in the contract.
121            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
164/// Checks if the sender is an admin or a member of a group.
165fn 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}