1use std::str::FromStr;
2
3use astroport::asset::addr_opt_validate;
4use astroport::staking;
5#[cfg(not(feature = "library"))]
6use cosmwasm_std::entry_point;
7use cosmwasm_std::{
8 attr, coins, ensure, wasm_execute, Addr, Api, BankMsg, CosmosMsg, Decimal, DepsMut, Env,
9 MessageInfo, QuerierWrapper, Response, StdError, Storage, SubMsg, Uint128, Uint64, WasmMsg,
10};
11use cw2::set_contract_version;
12use cw_utils::must_pay;
13use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg;
14
15use astroport_governance::assembly::{
16 validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalStatus,
17 ProposalVoteOption, UpdateConfig,
18};
19use astroport_governance::emissions_controller::hub::HubMsg;
20use astroport_governance::utils::check_contract_supports_channel;
21use astroport_governance::{emissions_controller, voting_escrow};
22
23use crate::error::ContractError;
24use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS};
25use crate::utils::{calc_total_voting_power_at, calc_voting_power};
26
27pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
29pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
30
31#[cfg_attr(not(feature = "library"), entry_point)]
33pub fn instantiate(
34 deps: DepsMut,
35 _env: Env,
36 _info: MessageInfo,
37 msg: InstantiateMsg,
38) -> Result<Response, ContractError> {
39 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
40
41 if msg.whitelisted_links.is_empty() {
42 return Err(ContractError::WhitelistEmpty {});
43 }
44
45 validate_links(&msg.whitelisted_links)?;
46
47 let staking_config = deps
48 .querier
49 .query_wasm_smart::<staking::Config>(&msg.staking_addr, &staking::QueryMsg::Config {})?;
50
51 let tracker_config = deps.querier.query_wasm_smart::<staking::TrackerData>(
52 &msg.staking_addr,
53 &staking::QueryMsg::TrackerConfig {},
54 )?;
55
56 let config = Config {
57 xastro_denom: staking_config.xastro_denom,
58 xastro_denom_tracking: tracker_config.tracker_addr,
59 vxastro_contract: None,
60 emissions_controller: None,
61 ibc_controller: addr_opt_validate(deps.api, &msg.ibc_controller)?,
62 builder_unlock_addr: deps.api.addr_validate(&msg.builder_unlock_addr)?,
63 proposal_voting_period: msg.proposal_voting_period,
64 proposal_effective_delay: msg.proposal_effective_delay,
65 proposal_expiration_period: msg.proposal_expiration_period,
66 proposal_required_deposit: msg.proposal_required_deposit,
67 proposal_required_quorum: Decimal::from_str(&msg.proposal_required_quorum)?,
68 proposal_required_threshold: Decimal::from_str(&msg.proposal_required_threshold)?,
69 whitelisted_links: msg.whitelisted_links,
70 };
71
72 #[cfg(not(feature = "testnet"))]
73 config.validate()?;
74
75 CONFIG.save(deps.storage, &config)?;
76
77 PROPOSAL_COUNT.save(deps.storage, &Uint64::zero())?;
78
79 Ok(Response::default())
80}
81
82#[cfg_attr(not(feature = "library"), entry_point)]
106pub fn execute(
107 deps: DepsMut,
108 env: Env,
109 info: MessageInfo,
110 msg: ExecuteMsg,
111) -> Result<Response, ContractError> {
112 match msg {
113 ExecuteMsg::SubmitProposal {
114 title,
115 description,
116 link,
117 messages,
118 ibc_channel,
119 } => submit_proposal(
120 deps,
121 env,
122 info,
123 title,
124 description,
125 link,
126 messages,
127 ibc_channel,
128 ),
129 ExecuteMsg::CastVote { proposal_id, vote } => {
130 let voter = info.sender.to_string();
131 let proposal = PROPOSALS.load(deps.storage, proposal_id)?;
132
133 let voting_power = calc_voting_power(deps.as_ref(), voter.clone(), &proposal)?;
134 ensure!(!voting_power.is_zero(), ContractError::NoVotingPower {});
135
136 cast_vote(
137 deps.storage,
138 env,
139 voter,
140 voting_power,
141 proposal_id,
142 proposal,
143 vote,
144 )
145 }
146 ExecuteMsg::CastVoteOutpost {
147 voter,
148 voting_power,
149 proposal_id,
150 vote,
151 } => {
152 let config = CONFIG.load(deps.storage)?;
153 ensure!(
154 Some(info.sender) == config.emissions_controller,
155 ContractError::Unauthorized {}
156 );
157
158 (|| {
161 let proposal = PROPOSALS.load(deps.storage, proposal_id)?;
162
163 cast_vote(
164 deps.storage,
165 env,
166 voter,
167 voting_power,
168 proposal_id,
169 proposal,
170 vote,
171 )
172 })()
173 .or_else(|err| {
174 Ok(Response::new()
175 .add_attribute("action", "cast_vote")
176 .add_attribute("error", err.to_string()))
177 })
178 }
179 ExecuteMsg::EndProposal { proposal_id } => end_proposal(deps, env, proposal_id),
180 ExecuteMsg::ExecuteProposal { proposal_id } => execute_proposal(deps, env, proposal_id),
181 ExecuteMsg::CheckMessages(messages) => check_messages(deps.api, env, messages),
182 ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}),
183 ExecuteMsg::UpdateConfig(config) => update_config(deps, env, info, config),
184 ExecuteMsg::IBCProposalCompleted {
185 proposal_id,
186 status,
187 } => update_ibc_proposal_status(deps, info, proposal_id, status),
188 ExecuteMsg::ExecuteFromMultisig(proposal_messages) => {
189 exec_from_multisig(deps.querier, info, env, proposal_messages)
190 }
191 }
192}
193
194#[allow(clippy::too_many_arguments)]
208pub fn submit_proposal(
209 deps: DepsMut,
210 env: Env,
211 info: MessageInfo,
212 title: String,
213 description: String,
214 link: Option<String>,
215 messages: Vec<CosmosMsg>,
216 ibc_channel: Option<String>,
217) -> Result<Response, ContractError> {
218 let config = CONFIG.load(deps.storage)?;
219
220 let deposit_amount = must_pay(&info, &config.xastro_denom)?;
223
224 if deposit_amount < config.proposal_required_deposit {
225 return Err(ContractError::InsufficientDeposit {});
226 }
227
228 let count = PROPOSAL_COUNT.update::<_, StdError>(deps.storage, |c| Ok(c + Uint64::one()))?;
230
231 if let Some(ibc_channel) = &ibc_channel {
233 if let Some(ibc_controller) = &config.ibc_controller {
234 check_contract_supports_channel(deps.querier, ibc_controller, ibc_channel)?;
235 } else {
236 return Err(ContractError::MissingIBCController {});
237 }
238 }
239
240 let proposal = Proposal {
241 proposal_id: count,
242 submitter: info.sender.clone(),
243 status: ProposalStatus::Active,
244 for_power: Uint128::zero(),
245 against_power: Uint128::zero(),
246 start_block: env.block.height,
247 start_time: env.block.time.seconds(),
248 end_block: env.block.height + config.proposal_voting_period,
249 delayed_end_block: env.block.height
250 + config.proposal_voting_period
251 + config.proposal_effective_delay,
252 expiration_block: env.block.height
253 + config.proposal_voting_period
254 + config.proposal_effective_delay
255 + config.proposal_expiration_period,
256 title,
257 description,
258 link,
259 messages,
260 deposit_amount,
261 ibc_channel,
262 total_voting_power: calc_total_voting_power_at(
265 deps.querier,
266 &config,
267 env.block.time.seconds() - 1,
268 )?,
269 };
270
271 proposal.validate(config.whitelisted_links)?;
272
273 PROPOSALS.save(deps.storage, count.u64(), &proposal)?;
274
275 let mut response = Response::new().add_attributes([
276 attr("action", "submit_proposal"),
277 attr("submitter", info.sender),
278 attr("proposal_id", count),
279 attr(
280 "proposal_end_height",
281 (env.block.height + config.proposal_voting_period).to_string(),
282 ),
283 ]);
284
285 if let Some(emissions_controller) = config.emissions_controller {
286 let outposts_register_msg = wasm_execute(
288 emissions_controller,
289 &emissions_controller::msg::ExecuteMsg::Custom(HubMsg::RegisterProposal {
290 proposal_id: count.u64(),
291 }),
292 vec![],
293 )?;
294 response = response.add_message(outposts_register_msg);
295 }
296
297 Ok(response)
298}
299
300pub fn cast_vote(
312 storage: &mut dyn Storage,
313 env: Env,
314 voter: String,
315 voting_power: Uint128,
316 proposal_id: u64,
317 mut proposal: Proposal,
318 vote_option: ProposalVoteOption,
319) -> Result<Response, ContractError> {
320 if proposal.status != ProposalStatus::Active {
321 return Err(ContractError::ProposalNotActive {});
322 }
323
324 if env.block.height > proposal.end_block {
325 return Err(ContractError::VotingPeriodEnded {});
326 }
327
328 if PROPOSAL_VOTERS.has(storage, (proposal_id, voter.clone())) {
329 return Err(ContractError::UserAlreadyVoted {});
330 }
331
332 match vote_option {
333 ProposalVoteOption::For => {
334 proposal.for_power = proposal.for_power.checked_add(voting_power)?;
335 }
336 ProposalVoteOption::Against => {
337 proposal.against_power = proposal.against_power.checked_add(voting_power)?;
338 }
339 };
340 PROPOSAL_VOTERS.save(storage, (proposal_id, voter.clone()), &vote_option)?;
341
342 PROPOSALS.save(storage, proposal_id, &proposal)?;
343
344 Ok(Response::new().add_attributes(vec![
345 attr("action", "cast_vote"),
346 attr("proposal_id", proposal_id.to_string()),
347 attr("voter", &voter),
348 attr("vote", vote_option.to_string()),
349 attr("voting_power", voting_power),
350 ]))
351}
352
353pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result<Response, ContractError> {
356 let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;
357
358 if proposal.status != ProposalStatus::Active {
359 return Err(ContractError::ProposalNotActive {});
360 }
361
362 if env.block.height <= proposal.end_block {
363 return Err(ContractError::VotingPeriodNotEnded {});
364 }
365
366 let config = CONFIG.load(deps.storage)?;
367
368 let for_votes = proposal.for_power;
369 let against_votes = proposal.against_power;
370 let total_votes = for_votes + against_votes;
371
372 let proposal_quorum =
373 Decimal::checked_from_ratio(total_votes, proposal.total_voting_power).unwrap_or_default();
374 let proposal_threshold =
375 Decimal::checked_from_ratio(for_votes, total_votes).unwrap_or_default();
376
377 proposal.status = if proposal_quorum >= config.proposal_required_quorum
379 && proposal_threshold > config.proposal_required_threshold
380 {
381 ProposalStatus::Passed
382 } else {
383 ProposalStatus::Rejected
384 };
385
386 PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
387
388 let response = Response::new()
389 .add_attributes([
390 attr("action", "end_proposal"),
391 attr("proposal_id", proposal_id.to_string()),
392 attr("proposal_result", proposal.status.to_string()),
393 ])
394 .add_message(BankMsg::Send {
395 to_address: proposal.submitter.to_string(),
396 amount: coins(proposal.deposit_amount.into(), config.xastro_denom),
397 });
398
399 Ok(response)
400}
401
402pub fn execute_proposal(
404 deps: DepsMut,
405 env: Env,
406 proposal_id: u64,
407) -> Result<Response, ContractError> {
408 let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;
409
410 if proposal.status != ProposalStatus::Passed {
411 return Err(ContractError::ProposalNotPassed {});
412 }
413
414 if env.block.height < proposal.delayed_end_block {
415 return Err(ContractError::ProposalDelayNotEnded {});
416 }
417
418 let mut response = Response::new().add_attributes([
419 attr("action", "execute_proposal"),
420 attr("proposal_id", proposal_id.to_string()),
421 ]);
422
423 if env.block.height > proposal.expiration_block {
424 proposal.status = ProposalStatus::Expired;
425 } else if let Some(channel) = &proposal.ibc_channel {
426 if !proposal.messages.is_empty() {
427 let config = CONFIG.load(deps.storage)?;
428
429 proposal.status = ProposalStatus::InProgress;
430 response.messages.push(SubMsg::new(wasm_execute(
431 config
432 .ibc_controller
433 .ok_or(ContractError::MissingIBCController {})?,
434 &ControllerExecuteMsg::IbcExecuteProposal {
435 channel_id: channel.to_string(),
436 proposal_id,
437 messages: proposal.messages.clone(),
438 },
439 vec![],
440 )?))
441 } else {
442 proposal.status = ProposalStatus::Executed;
443 }
444 } else {
445 proposal.status = ProposalStatus::Executed;
446 response
447 .messages
448 .extend(proposal.messages.iter().cloned().map(SubMsg::new))
449 }
450
451 PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
452
453 Ok(response.add_attribute("proposal_status", proposal.status.to_string()))
454}
455
456pub fn check_messages(
458 api: &dyn Api,
459 env: Env,
460 mut messages: Vec<CosmosMsg>,
461) -> Result<Response, ContractError> {
462 messages.iter().try_for_each(|msg| match msg {
463 CosmosMsg::Wasm(
464 WasmMsg::Migrate { contract_addr, .. } | WasmMsg::UpdateAdmin { contract_addr, .. },
465 ) if api.addr_validate(contract_addr)? == env.contract.address => {
466 Err(StdError::generic_err(
467 "Can't check messages with a migration or update admin message of the contract itself",
468 ))
469 }
470 CosmosMsg::Stargate { type_url, .. } if type_url.contains("MsgGrant") => Err(
471 StdError::generic_err("Can't check messages with a MsgGrant message"),
472 ),
473 _ => Ok(()),
474 })?;
475
476 messages.push(
477 wasm_execute(
478 env.contract.address,
479 &ExecuteMsg::CheckMessagesPassed {},
480 vec![],
481 )?
482 .into(),
483 );
484
485 Ok(Response::new()
486 .add_attribute("action", "check_messages")
487 .add_messages(messages))
488}
489
490pub fn update_config(
494 deps: DepsMut,
495 env: Env,
496 info: MessageInfo,
497 updated_config: Box<UpdateConfig>,
498) -> Result<Response, ContractError> {
499 let mut config = CONFIG.load(deps.storage)?;
500
501 if info.sender != env.contract.address {
503 return Err(ContractError::Unauthorized {});
504 }
505
506 let mut attrs = vec![attr("action", "update_config")];
507
508 if let Some(ibc_controller) = updated_config.ibc_controller {
509 config.ibc_controller = Some(deps.api.addr_validate(&ibc_controller)?);
510 attrs.push(attr("new_ibc_controller", ibc_controller));
511 }
512
513 if let Some(builder_unlock_addr) = updated_config.builder_unlock_addr {
514 config.builder_unlock_addr = deps.api.addr_validate(&builder_unlock_addr)?;
515 attrs.push(attr("new_builder_unlock_addr", builder_unlock_addr));
516 }
517
518 if let Some(proposal_voting_period) = updated_config.proposal_voting_period {
519 config.proposal_voting_period = proposal_voting_period;
520 attrs.push(attr(
521 "new_proposal_voting_period",
522 proposal_voting_period.to_string(),
523 ));
524 }
525
526 if let Some(proposal_effective_delay) = updated_config.proposal_effective_delay {
527 config.proposal_effective_delay = proposal_effective_delay;
528 attrs.push(attr(
529 "new_proposal_effective_delay",
530 proposal_effective_delay.to_string(),
531 ));
532 }
533
534 if let Some(proposal_expiration_period) = updated_config.proposal_expiration_period {
535 config.proposal_expiration_period = proposal_expiration_period;
536 attrs.push(attr(
537 "new_proposal_expiration_period",
538 proposal_expiration_period.to_string(),
539 ));
540 }
541
542 if let Some(proposal_required_deposit) = updated_config.proposal_required_deposit {
543 config.proposal_required_deposit = proposal_required_deposit;
544 attrs.push(attr(
545 "new_proposal_required_deposit",
546 proposal_required_deposit.to_string(),
547 ));
548 }
549
550 if let Some(proposal_required_quorum) = updated_config.proposal_required_quorum {
551 config.proposal_required_quorum = proposal_required_quorum;
552 attrs.push(attr(
553 "new_proposal_required_quorum",
554 proposal_required_quorum.to_string(),
555 ));
556 }
557
558 if let Some(proposal_required_threshold) = updated_config.proposal_required_threshold {
559 config.proposal_required_threshold = proposal_required_threshold;
560 attrs.push(attr(
561 "new_proposal_required_threshold",
562 proposal_required_threshold.to_string(),
563 ));
564 }
565
566 if let Some(whitelist_add) = updated_config.whitelist_add {
567 validate_links(&whitelist_add)?;
568
569 let mut new_links = whitelist_add
570 .into_iter()
571 .filter(|link| !config.whitelisted_links.contains(link))
572 .collect::<Vec<_>>();
573
574 attrs.push(attr("new_whitelisted_links", new_links.join(", ")));
575
576 config.whitelisted_links.append(&mut new_links);
577 }
578
579 if let Some(whitelist_remove) = updated_config.whitelist_remove {
580 config
581 .whitelisted_links
582 .retain(|link| !whitelist_remove.contains(link));
583
584 attrs.push(attr(
585 "removed_whitelisted_links",
586 whitelist_remove.join(", "),
587 ));
588
589 if config.whitelisted_links.is_empty() {
590 return Err(ContractError::WhitelistEmpty {});
591 }
592 }
593
594 if let Some(vxastro) = updated_config.vxastro {
595 let emissions_controller = deps
596 .querier
597 .query_wasm_smart::<voting_escrow::Config>(
598 &vxastro,
599 &voting_escrow::QueryMsg::Config {},
600 )?
601 .emissions_controller;
602
603 config.emissions_controller = Some(Addr::unchecked(&emissions_controller));
604 config.vxastro_contract = Some(Addr::unchecked(&vxastro));
605
606 attrs.push(attr("new_emissions_controller", emissions_controller));
607 attrs.push(attr("new_vxastro_contract", vxastro));
608 }
609
610 #[cfg(not(feature = "testnet"))]
611 config.validate()?;
612
613 CONFIG.save(deps.storage, &config)?;
614
615 Ok(Response::new().add_attributes(attrs))
616}
617
618fn update_ibc_proposal_status(
625 deps: DepsMut,
626 info: MessageInfo,
627 id: u64,
628 new_status: ProposalStatus,
629) -> Result<Response, ContractError> {
630 let config = CONFIG.load(deps.storage)?;
631 if Some(info.sender) == config.ibc_controller {
632 let mut proposal = PROPOSALS.load(deps.storage, id)?;
633
634 if proposal.status != ProposalStatus::InProgress {
635 return Err(ContractError::WrongIbcProposalStatus(
636 proposal.status.to_string(),
637 ));
638 }
639
640 match new_status {
641 ProposalStatus::Executed {} | ProposalStatus::Failed {} => {
642 proposal.status = new_status;
643 PROPOSALS.save(deps.storage, id, &proposal)?;
644 Ok(Response::new().add_attribute("action", "ibc_proposal_completed"))
645 }
646 _ => Err(ContractError::InvalidRemoteIbcProposalStatus(
647 new_status.to_string(),
648 )),
649 }
650 } else {
651 Err(ContractError::InvalidIBCController {})
652 }
653}
654
655pub fn exec_from_multisig(
656 querier: QuerierWrapper,
657 info: MessageInfo,
658 env: Env,
659 messages: Vec<CosmosMsg>,
660) -> Result<Response, ContractError> {
661 match querier
662 .query_wasm_contract_info(&env.contract.address)?
663 .admin
664 {
665 None => Err(ContractError::Unauthorized {}),
666 Some(admin) if admin != info.sender || admin == env.contract.address => {
668 Err(ContractError::Unauthorized {})
669 }
670 _ => Ok(()),
671 }?;
672
673 Ok(Response::new().add_messages(messages))
674}