cw4_voting/
contract.rs

1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4    to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult,
5    SubMsg, Uint128, WasmMsg,
6};
7use cw2::set_contract_version;
8use cw_utils::parse_reply_instantiate_data;
9
10use crate::error::ContractError;
11use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
12use crate::state::{DAO_ADDRESS, GROUP_CONTRACT, TOTAL_WEIGHT, USER_WEIGHTS};
13
14const CONTRACT_NAME: &str = "crates.io:cw4-voting";
15const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
16
17const INSTANTIATE_GROUP_REPLY_ID: u64 = 0;
18
19#[cfg_attr(not(feature = "library"), entry_point)]
20pub fn instantiate(
21    deps: DepsMut,
22    env: Env,
23    info: MessageInfo,
24    msg: InstantiateMsg,
25) -> Result<Response, ContractError> {
26    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
27    if msg.initial_members.is_empty() {
28        return Err(ContractError::NoMembers {});
29    }
30    let original_len = msg.initial_members.len();
31    let mut initial_members = msg.initial_members;
32    initial_members.sort_by(|a, b| a.addr.cmp(&b.addr));
33    initial_members.dedup();
34    let new_len = initial_members.len();
35
36    if original_len != new_len {
37        return Err(ContractError::DuplicateMembers {});
38    }
39
40    let mut total_weight = Uint128::zero();
41    for member in initial_members.iter() {
42        let member_addr = deps.api.addr_validate(&member.addr)?;
43        if member.weight > 0 {
44            // This works because query_voting_power_at_height will return 0 on address missing
45            // from storage, so no need to store anything.
46            let weight = Uint128::from(member.weight);
47            USER_WEIGHTS.save(deps.storage, &member_addr, &weight, env.block.height)?;
48            total_weight += weight;
49        }
50    }
51
52    if total_weight.is_zero() {
53        return Err(ContractError::ZeroTotalWeight {});
54    }
55    TOTAL_WEIGHT.save(deps.storage, &total_weight, env.block.height)?;
56
57    // We need to set ourself as the CW4 admin it is then transferred to the DAO in the reply
58    let msg = WasmMsg::Instantiate {
59        admin: Some(info.sender.to_string()),
60        code_id: msg.cw4_group_code_id,
61        msg: to_binary(&cw4_group::msg::InstantiateMsg {
62            admin: Some(env.contract.address.to_string()),
63            members: initial_members,
64        })?,
65        funds: vec![],
66        label: env.contract.address.to_string(),
67    };
68
69    let msg = SubMsg::reply_on_success(msg, INSTANTIATE_GROUP_REPLY_ID);
70
71    DAO_ADDRESS.save(deps.storage, &info.sender)?;
72
73    Ok(Response::new()
74        .add_attribute("action", "instantiate")
75        .add_submessage(msg))
76}
77
78#[cfg_attr(not(feature = "library"), entry_point)]
79pub fn execute(
80    deps: DepsMut,
81    env: Env,
82    info: MessageInfo,
83    msg: ExecuteMsg,
84) -> Result<Response, ContractError> {
85    match msg {
86        ExecuteMsg::MemberChangedHook { diffs } => {
87            execute_member_changed_hook(deps, env, info, diffs)
88        }
89    }
90}
91
92pub fn execute_member_changed_hook(
93    deps: DepsMut,
94    env: Env,
95    info: MessageInfo,
96    diffs: Vec<cw4::MemberDiff>,
97) -> Result<Response, ContractError> {
98    let group_contract = GROUP_CONTRACT.load(deps.storage)?;
99    if info.sender != group_contract {
100        return Err(ContractError::Unauthorized {});
101    }
102
103    let total_weight = TOTAL_WEIGHT.load(deps.storage)?;
104    // As difference can be negative we need to keep track of both
105    // In seperate counters to apply at once and prevent underflow
106    let mut positive_difference: Uint128 = Uint128::zero();
107    let mut negative_difference: Uint128 = Uint128::zero();
108    for diff in diffs {
109        let user_address = deps.api.addr_validate(&diff.key)?;
110        let weight = diff.new.unwrap_or_default();
111        let old = diff.old.unwrap_or_default();
112        // Do we need to add to positive difference or negative difference
113        if weight > old {
114            positive_difference += Uint128::from(weight - old);
115        } else {
116            negative_difference += Uint128::from(old - weight);
117        }
118
119        if weight != 0 {
120            USER_WEIGHTS.save(
121                deps.storage,
122                &user_address,
123                &Uint128::from(weight),
124                env.block.height,
125            )?;
126        } else if weight == 0 && weight != old {
127            // This works because query_voting_power_at_height will return 0 on address missing
128            // from storage, so no need to store anything.
129            //
130            // Note that we also check for weight != old: If for some reason this hook is triggered
131            // with weight 0 for old and new values, we don't need to do anything.
132            USER_WEIGHTS.remove(deps.storage, &user_address, env.block.height)?;
133        }
134    }
135    let new_total_weight = total_weight
136        .checked_add(positive_difference)
137        .map_err(StdError::overflow)?
138        .checked_sub(negative_difference)
139        .map_err(StdError::overflow)?;
140    TOTAL_WEIGHT.save(deps.storage, &new_total_weight, env.block.height)?;
141
142    Ok(Response::new()
143        .add_attribute("action", "member_changed_hook")
144        .add_attribute("total_weight", new_total_weight.to_string()))
145}
146
147#[cfg_attr(not(feature = "library"), entry_point)]
148pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
149    match msg {
150        QueryMsg::VotingPowerAtHeight { address, height } => {
151            query_voting_power_at_height(deps, env, address, height)
152        }
153        QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height),
154        QueryMsg::Info {} => query_info(deps),
155        QueryMsg::GroupContract {} => to_binary(&GROUP_CONTRACT.load(deps.storage)?),
156        QueryMsg::Dao {} => to_binary(&DAO_ADDRESS.load(deps.storage)?),
157    }
158}
159
160pub fn query_voting_power_at_height(
161    deps: Deps,
162    env: Env,
163    address: String,
164    height: Option<u64>,
165) -> StdResult<Binary> {
166    let address = deps.api.addr_validate(&address)?;
167    let height = height.unwrap_or(env.block.height);
168    let power = USER_WEIGHTS
169        .may_load_at_height(deps.storage, &address, height)?
170        .unwrap_or_default();
171
172    to_binary(&cw_core_interface::voting::VotingPowerAtHeightResponse { power, height })
173}
174
175pub fn query_total_power_at_height(deps: Deps, env: Env, height: Option<u64>) -> StdResult<Binary> {
176    let height = height.unwrap_or(env.block.height);
177    let power = TOTAL_WEIGHT
178        .may_load_at_height(deps.storage, height)?
179        .unwrap_or_default();
180    to_binary(&cw_core_interface::voting::TotalPowerAtHeightResponse { power, height })
181}
182
183pub fn query_info(deps: Deps) -> StdResult<Binary> {
184    let info = cw2::get_contract_version(deps.storage)?;
185    to_binary(&cw_core_interface::voting::InfoResponse { info })
186}
187
188#[cfg_attr(not(feature = "library"), entry_point)]
189pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
190    // Don't do any state migrations.
191    Ok(Response::default())
192}
193
194#[cfg_attr(not(feature = "library"), entry_point)]
195pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
196    match msg.id {
197        INSTANTIATE_GROUP_REPLY_ID => {
198            let res = parse_reply_instantiate_data(msg);
199            match res {
200                Ok(res) => {
201                    let group_contract = GROUP_CONTRACT.may_load(deps.storage)?;
202                    if group_contract.is_some() {
203                        return Err(ContractError::DuplicateGroupContract {});
204                    }
205                    let group_contract = deps.api.addr_validate(&res.contract_address)?;
206                    let dao_address = DAO_ADDRESS.load(deps.storage)?;
207                    GROUP_CONTRACT.save(deps.storage, &group_contract)?;
208                    let msg1 = WasmMsg::Execute {
209                        contract_addr: group_contract.to_string(),
210                        msg: to_binary(&cw4_group::msg::ExecuteMsg::AddHook {
211                            addr: env.contract.address.to_string(),
212                        })?,
213                        funds: vec![],
214                    };
215                    // Transfer admin status to the DAO
216                    let msg2 = WasmMsg::Execute {
217                        contract_addr: group_contract.to_string(),
218                        msg: to_binary(&cw4_group::msg::ExecuteMsg::UpdateAdmin {
219                            admin: Some(dao_address.to_string()),
220                        })?,
221                        funds: vec![],
222                    };
223                    Ok(Response::default()
224                        .add_attribute("group_contract_address", group_contract)
225                        .add_message(msg1)
226                        .add_message(msg2))
227                }
228                Err(_) => Err(ContractError::GroupContractInstantiateError {}),
229            }
230        }
231        _ => Err(ContractError::UnknownReplyId { id: msg.id }),
232    }
233}