1use std::collections::{HashMap, HashSet};
2
3use astroport::asset::{determine_asset_info, validate_native_denom};
4use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner};
5use astroport::incentives;
6#[cfg(not(feature = "library"))]
7use cosmwasm_std::entry_point;
8use cosmwasm_std::{
9 attr, ensure, ensure_eq, to_json_binary, wasm_execute, BankMsg, Coin, CosmosMsg, Decimal,
10 DepsMut, Env, Fraction, IbcMsg, IbcTimeout, MessageInfo, Order, Response, StdError, StdResult,
11 Storage, Uint128,
12};
13use cw_utils::{must_pay, nonpayable};
14use itertools::Itertools;
15use neutron_sdk::bindings::msg::NeutronMsg;
16use neutron_sdk::bindings::query::NeutronQuery;
17
18use astroport_governance::emissions_controller::consts::{EPOCH_LENGTH, IBC_TIMEOUT};
19use astroport_governance::emissions_controller::hub::{
20 AstroPoolConfig, HubMsg, InputOutpostParams, OutpostInfo, OutpostParams, OutpostStatus,
21 TuneInfo, UserInfo, VotedPoolInfo,
22};
23use astroport_governance::emissions_controller::msg::{ExecuteMsg, VxAstroIbcMsg};
24use astroport_governance::emissions_controller::utils::{check_lp_token, get_voting_power};
25use astroport_governance::utils::{
26 check_contract_supports_channel, determine_ics20_escrow_address,
27};
28use astroport_governance::{assembly, voting_escrow};
29
30use crate::error::ContractError;
31use crate::state::{
32 get_active_outposts, CONFIG, OUTPOSTS, OWNERSHIP_PROPOSAL, POOLS_BLACKLIST, POOLS_WHITELIST,
33 TUNE_INFO, USER_INFO, VOTED_POOLS,
34};
35use crate::utils::{
36 build_emission_ibc_msg, get_epoch_start, get_outpost_prefix, jail_outpost, min_ntrn_ibc_fee,
37 raw_emissions_to_schedules, simulate_tune, validate_outpost_prefix, TuneResult,
38};
39
40#[cfg_attr(not(feature = "library"), entry_point)]
42pub fn execute(
43 deps: DepsMut<NeutronQuery>,
44 env: Env,
45 info: MessageInfo,
46 msg: ExecuteMsg<HubMsg>,
47) -> Result<Response<NeutronMsg>, ContractError> {
48 match msg {
49 ExecuteMsg::Vote { votes } => {
50 nonpayable(&info)?;
51 let votes_map: HashMap<_, _> = votes.iter().cloned().collect();
52 ensure!(
53 votes.len() == votes_map.len(),
54 ContractError::DuplicatedVotes {}
55 );
56 let deps = deps.into_empty();
57 let config = CONFIG.load(deps.storage)?;
58 let voting_power = get_voting_power(deps.querier, &config.vxastro, &info.sender, None)?;
59 ensure!(!voting_power.is_zero(), ContractError::ZeroVotingPower {});
60
61 handle_vote(deps, env, info.sender.as_str(), voting_power, votes_map)
62 }
63 ExecuteMsg::UpdateUserVotes { user, is_unlock } => {
64 let config = CONFIG.load(deps.storage)?;
65 ensure!(
66 info.sender == config.vxastro,
67 ContractError::Unauthorized {}
68 );
69 let voter = deps.api.addr_validate(&user)?;
70 let deps = deps.into_empty();
71
72 let voting_power = get_voting_power(deps.querier, &config.vxastro, &voter, None)?;
73 handle_update_user(deps.storage, env, voter.as_str(), voting_power).and_then(
74 |response| {
75 if is_unlock {
76 let confirm_unlock_msg = wasm_execute(
77 config.vxastro,
78 &voting_escrow::ExecuteMsg::ConfirmUnlock {
79 user: voter.to_string(),
80 },
81 vec![],
82 )?;
83 Ok(response.add_message(confirm_unlock_msg))
84 } else {
85 Ok(response)
86 }
87 },
88 )
89 }
90 ExecuteMsg::RefreshUserVotes {} => {
91 nonpayable(&info)?;
92 let config = CONFIG.load(deps.storage)?;
93 let deps = deps.into_empty();
94
95 let voting_power = get_voting_power(deps.querier, &config.vxastro, &info.sender, None)?;
96
97 ensure!(!voting_power.is_zero(), ContractError::ZeroVotingPower {});
98 handle_update_user(deps.storage, env, info.sender.as_str(), voting_power)
99 }
100 ExecuteMsg::ProposeNewOwner {
101 new_owner,
102 expires_in,
103 } => {
104 nonpayable(&info)?;
105 let config = CONFIG.load(deps.storage)?;
106
107 propose_new_owner(
108 deps,
109 info,
110 env,
111 new_owner,
112 expires_in,
113 config.owner,
114 OWNERSHIP_PROPOSAL,
115 )
116 .map_err(Into::into)
117 }
118 ExecuteMsg::DropOwnershipProposal {} => {
119 nonpayable(&info)?;
120 let config = CONFIG.load(deps.storage)?;
121
122 drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL)
123 .map_err(Into::into)
124 }
125 ExecuteMsg::ClaimOwnership {} => {
126 nonpayable(&info)?;
127 claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| {
128 CONFIG
129 .update::<_, StdError>(deps.storage, |mut v| {
130 v.owner = new_owner;
131 Ok(v)
132 })
133 .map(|_| ())
134 })
135 .map_err(Into::into)
136 }
137 ExecuteMsg::Custom(hub_msg) => match hub_msg {
138 HubMsg::WhitelistPool { lp_token: pool } => whitelist_pool(deps, env, info, pool),
139 HubMsg::UpdateBlacklist { add, remove } => {
140 update_blacklist(deps, info, env, add, remove)
141 }
142 HubMsg::UpdateOutpost {
143 prefix,
144 astro_denom,
145 outpost_params,
146 astro_pool_config,
147 } => update_outpost(
148 deps,
149 env,
150 info,
151 prefix,
152 astro_denom,
153 outpost_params,
154 astro_pool_config,
155 ),
156 HubMsg::JailOutpost { prefix } => jail_outpost_endpoint(deps, env, info, prefix),
157 HubMsg::UnjailOutpost { prefix } => unjail_outpost(deps, info, prefix),
158 HubMsg::TunePools {} => tune_pools(deps, env),
159 HubMsg::RetryFailedOutposts {} => retry_failed_outposts(deps, info, env),
160 HubMsg::UpdateConfig {
161 pools_per_outpost,
162 whitelisting_fee,
163 fee_receiver,
164 emissions_multiple,
165 max_astro,
166 } => update_config(
167 deps,
168 info,
169 pools_per_outpost,
170 whitelisting_fee,
171 fee_receiver,
172 emissions_multiple,
173 max_astro,
174 ),
175 HubMsg::RegisterProposal { proposal_id } => register_proposal(deps, env, proposal_id),
176 },
177 }
178}
179
180pub fn whitelist_pool(
184 deps: DepsMut<NeutronQuery>,
185 env: Env,
186 info: MessageInfo,
187 pool: String,
188) -> Result<Response<NeutronMsg>, ContractError> {
189 let deps = deps.into_empty();
190 let config = CONFIG.load(deps.storage)?;
191 let amount = must_pay(&info, &config.whitelisting_fee.denom)?;
192 ensure!(
193 amount == config.whitelisting_fee.amount,
194 ContractError::IncorrectWhitelistFee(config.whitelisting_fee)
195 );
196
197 ensure!(
199 !POOLS_BLACKLIST.has(deps.storage, &pool),
200 ContractError::PoolIsBlacklisted(pool.clone())
201 );
202
203 let outposts = get_active_outposts(deps.storage)?;
205 if let Some(prefix) = get_outpost_prefix(&pool, &outposts) {
206 if outposts.get(&prefix).unwrap().params.is_none() {
207 determine_asset_info(&pool, deps.api)
209 .and_then(|maybe_lp| check_lp_token(deps.querier, &config.factory, &maybe_lp))?
210 }
211 } else {
212 return Err(ContractError::NoOutpostForPool(pool));
213 }
214
215 ensure!(
217 outposts.values().all(|outpost_info| {
218 outpost_info
219 .astro_pool_config
220 .as_ref()
221 .map(|conf| conf.astro_pool != pool)
222 .unwrap_or(true)
223 }),
224 ContractError::IsAstroPool {}
225 );
226
227 POOLS_WHITELIST.update(deps.storage, |v| {
228 let mut pools: HashSet<_> = v.into_iter().collect();
229 if !pools.insert(pool.clone()) {
230 return Err(ContractError::PoolAlreadyWhitelisted(pool.clone()));
231 };
232 Ok(pools.into_iter().collect())
233 })?;
234
235 VOTED_POOLS.save(
237 deps.storage,
238 &pool,
239 &VotedPoolInfo {
240 init_ts: env.block.time.seconds(),
241 voting_power: Uint128::zero(),
242 },
243 env.block.time.seconds(),
244 )?;
245
246 let send_fee_msg = BankMsg::Send {
247 to_address: config.fee_receiver.to_string(),
248 amount: info.funds,
249 };
250
251 Ok(Response::default()
252 .add_message(send_fee_msg)
253 .add_attributes([attr("action", "whitelist_pool"), attr("pool", &pool)]))
254}
255
256pub fn update_blacklist(
257 deps: DepsMut<NeutronQuery>,
258 info: MessageInfo,
259 env: Env,
260 add: Vec<String>,
261 remove: Vec<String>,
262) -> Result<Response<NeutronMsg>, ContractError> {
263 let config = CONFIG.load(deps.storage)?;
264
265 ensure_eq!(info.sender, config.owner, ContractError::Unauthorized {});
266
267 ensure!(
269 remove.iter().chain(add.iter()).all_unique(),
270 StdError::generic_err("Duplicated LP tokens found")
271 );
272
273 for lp_token in &remove {
275 ensure!(
276 POOLS_BLACKLIST.has(deps.storage, lp_token),
277 StdError::generic_err(format!("LP token {lp_token} wasn't found in the blacklist"))
278 );
279
280 POOLS_BLACKLIST.remove(deps.storage, lp_token);
281 }
282
283 for lp_token in &add {
285 ensure!(
286 !POOLS_BLACKLIST.has(deps.storage, lp_token),
287 StdError::generic_err(format!("LP token {lp_token} is already blacklisted"))
288 );
289
290 VOTED_POOLS.remove(deps.storage, lp_token, env.block.time.seconds())?;
292 POOLS_BLACKLIST.save(deps.storage, lp_token, &())?;
293 }
294
295 POOLS_WHITELIST.update::<_, StdError>(deps.storage, |mut whitelist| {
297 whitelist.retain(|pool| !add.contains(pool));
298 Ok(whitelist)
299 })?;
300
301 let mut attrs = vec![attr("action", "update_blacklist")];
302
303 if !add.is_empty() {
304 attrs.push(attr("add", add.into_iter().join(",")));
305 }
306 if !remove.is_empty() {
307 attrs.push(attr("remove", remove.into_iter().join(",")));
308 }
309
310 Ok(Response::default().add_attributes(attrs))
311}
312
313pub fn update_outpost(
316 deps: DepsMut<NeutronQuery>,
317 env: Env,
318 info: MessageInfo,
319 prefix: String,
320 astro_denom: String,
321 outpost_params: Option<InputOutpostParams>,
322 astro_pool_config: Option<AstroPoolConfig>,
323) -> Result<Response<NeutronMsg>, ContractError> {
324 nonpayable(&info)?;
325 let deps = deps.into_empty();
326 let config = CONFIG.load(deps.storage)?;
327
328 ensure!(info.sender == config.owner, ContractError::Unauthorized {});
329
330 validate_native_denom(&astro_denom)?;
331 if let Some(conf) = &astro_pool_config {
332 validate_outpost_prefix(&conf.astro_pool, &prefix)?;
333 ensure!(
334 !conf.constant_emissions.is_zero(),
335 ContractError::ZeroAstroEmissions {}
336 );
337
338 POOLS_WHITELIST.update::<_, StdError>(deps.storage, |mut pools| {
340 pools.retain(|pool| pool != &conf.astro_pool);
341 Ok(pools)
342 })?;
343
344 VOTED_POOLS.remove(deps.storage, &conf.astro_pool, env.block.time.seconds())?;
346 }
347
348 if let Some(params) = &outpost_params {
349 validate_outpost_prefix(¶ms.emissions_controller, &prefix)?;
350 ensure!(
351 astro_denom.starts_with("ibc/") && astro_denom.len() == 68,
352 ContractError::InvalidOutpostAstroDenom {}
353 );
354 check_contract_supports_channel(
355 deps.as_ref().into_empty().querier,
356 &env.contract.address,
357 ¶ms.voting_channel,
358 )?;
359 ensure!(
360 params.ics20_channel.starts_with("channel-"),
361 ContractError::InvalidOutpostIcs20Channel {}
362 );
363 } else {
364 if let Some(conf) = &astro_pool_config {
365 let maybe_lp_token = determine_asset_info(&conf.astro_pool, deps.api)?;
366 check_lp_token(deps.querier, &config.factory, &maybe_lp_token)?;
367 }
368 ensure!(
369 astro_denom == config.astro_denom,
370 ContractError::InvalidHubAstroDenom(config.astro_denom)
371 );
372 }
373
374 OUTPOSTS.update(deps.storage, &prefix, |outpost| match outpost {
375 Some(OutpostInfo { jailed: true, .. }) => Err(ContractError::JailedOutpost {
376 prefix: prefix.clone(),
377 }),
378 _ => {
379 let params = outpost_params
380 .map(|params| -> StdResult<_> {
381 Ok(OutpostParams {
382 emissions_controller: params.emissions_controller,
383 voting_channel: params.voting_channel,
384 escrow_address: determine_ics20_escrow_address(
385 deps.api,
386 "transfer",
387 ¶ms.ics20_channel,
388 )?,
389 ics20_channel: params.ics20_channel,
390 })
391 })
392 .transpose()?;
393
394 Ok(OutpostInfo {
395 params,
396 astro_denom,
397 astro_pool_config,
398 jailed: false,
399 })
400 }
401 })?;
402
403 Ok(Response::default().add_attributes([("action", "update_outpost"), ("prefix", &prefix)]))
404}
405
406pub fn jail_outpost_endpoint(
409 deps: DepsMut<NeutronQuery>,
410 env: Env,
411 info: MessageInfo,
412 prefix: String,
413) -> Result<Response<NeutronMsg>, ContractError> {
414 nonpayable(&info)?;
415 let config = CONFIG.load(deps.storage)?;
416 ensure!(info.sender == config.owner, ContractError::Unauthorized {});
417
418 jail_outpost(deps.storage, &prefix, env)?;
419
420 Ok(Response::default().add_attributes([("action", "jail_outpost"), ("prefix", &prefix)]))
421}
422
423pub fn unjail_outpost(
424 deps: DepsMut<NeutronQuery>,
425 info: MessageInfo,
426 prefix: String,
427) -> Result<Response<NeutronMsg>, ContractError> {
428 nonpayable(&info)?;
429 let config = CONFIG.load(deps.storage)?;
430 ensure!(info.sender == config.owner, ContractError::Unauthorized {});
431
432 OUTPOSTS.update(deps.storage, &prefix, |outpost| {
433 if let Some(outpost) = outpost {
434 Ok(OutpostInfo {
435 jailed: false,
436 ..outpost
437 })
438 } else {
439 Err(ContractError::OutpostNotFound {
440 prefix: prefix.clone(),
441 })
442 }
443 })?;
444
445 Ok(Response::default().add_attributes([("action", "unjail_outpost"), ("prefix", &prefix)]))
446}
447
448pub fn retry_failed_outposts(
450 deps: DepsMut<NeutronQuery>,
451 info: MessageInfo,
452 env: Env,
453) -> Result<Response<NeutronMsg>, ContractError> {
454 nonpayable(&info)?;
455 let mut tune_info = TUNE_INFO.load(deps.storage)?;
456 let outposts = get_active_outposts(deps.storage)?;
457
458 let mut attrs = vec![attr("action", "retry_failed_outposts")];
459 let ibc_fee = min_ntrn_ibc_fee(deps.as_ref())?;
460 let config = CONFIG.load(deps.storage)?;
461
462 let retry_msgs = tune_info
463 .outpost_emissions_statuses
464 .iter_mut()
465 .filter_map(|(outpost, status)| {
466 let outpost_info = outposts.get(outpost)?;
467 outpost_info.params.as_ref().and_then(|params| {
468 if *status == OutpostStatus::Failed {
469 let raw_schedules = tune_info.pools_grouped.get(outpost)?;
470 let (schedules, astro_funds) = raw_emissions_to_schedules(
471 &env,
472 raw_schedules,
473 &outpost_info.astro_denom,
474 &config.astro_denom,
475 );
476 let msg =
478 build_emission_ibc_msg(&env, params, &ibc_fee, astro_funds, &schedules)
479 .ok()?;
480
481 *status = OutpostStatus::InProgress;
482 attrs.push(attr("outpost", outpost));
483
484 Some(msg)
485 } else {
486 None
487 }
488 })
489 })
490 .collect_vec();
491
492 ensure!(
493 !retry_msgs.is_empty(),
494 ContractError::NoFailedOutpostsToRetry {}
495 );
496
497 TUNE_INFO.save(deps.storage, &tune_info, env.block.time.seconds())?;
498
499 Ok(Response::new()
500 .add_messages(retry_msgs)
501 .add_attributes(attrs))
502}
503
504pub fn handle_vote(
516 deps: DepsMut,
517 env: Env,
518 voter: &str,
519 voting_power: Uint128,
520 votes: HashMap<String, Decimal>,
521) -> Result<Response<NeutronMsg>, ContractError> {
522 let user_info = USER_INFO.may_load(deps.storage, voter)?.unwrap_or_default();
523 let block_ts = env.block.time.seconds();
524
525 let epoch_start = get_epoch_start(block_ts);
526 ensure!(
528 user_info.vote_ts < epoch_start,
529 ContractError::VoteCooldown(epoch_start + EPOCH_LENGTH)
530 );
531
532 let mut total_weight = Decimal::zero();
533 let whitelist: HashSet<_> = POOLS_WHITELIST.load(deps.storage)?.into_iter().collect();
534 for (pool, weight) in &votes {
535 ensure!(
536 whitelist.contains(pool),
537 ContractError::PoolIsNotWhitelisted(pool.clone())
538 );
539
540 total_weight += weight;
541
542 ensure!(
543 total_weight <= Decimal::one(),
544 ContractError::InvalidTotalWeight {}
545 );
546 }
547
548 let mut cache = user_info
550 .votes
551 .into_iter()
552 .filter(|(pool, _)| whitelist.contains(pool))
553 .map(|(pool, weight)| {
554 let pool_info = VOTED_POOLS.load(deps.storage, &pool)?;
555 let pool_dedicated_vp = if pool_info.init_ts <= user_info.vote_ts {
557 user_info
558 .voting_power
559 .multiply_ratio(weight.numerator(), weight.denominator())
560 } else {
561 Uint128::zero()
562 };
563 Ok((pool, pool_info.with_sub_vp(pool_dedicated_vp)))
564 })
565 .collect::<StdResult<HashMap<_, _>>>()?;
566
567 votes
569 .iter()
570 .try_for_each(|(pool, weight)| -> StdResult<()> {
571 let pool_dedicated_vp =
572 voting_power.multiply_ratio(weight.numerator(), weight.denominator());
573
574 let pool_info = if let Some(pool_info) = cache.remove(pool) {
575 pool_info
576 } else {
577 VOTED_POOLS.load(deps.storage, pool)?
578 };
579
580 VOTED_POOLS.save(
581 deps.storage,
582 pool,
583 &pool_info.with_add_vp(pool_dedicated_vp),
584 block_ts,
585 )
586 })?;
587
588 cache.into_iter().try_for_each(|(pool, pool_info)| {
590 VOTED_POOLS.save(deps.storage, &pool, &pool_info, block_ts)
591 })?;
592
593 USER_INFO.save(
594 deps.storage,
595 voter,
596 &UserInfo {
597 vote_ts: block_ts,
598 voting_power,
599 votes,
600 },
601 block_ts,
602 )?;
603
604 Ok(Response::default()
605 .add_attributes([attr("action", "vote"), attr("voting_power", voting_power)]))
606}
607
608pub fn handle_update_user(
611 store: &mut dyn Storage,
612 env: Env,
613 voter: &str,
614 new_voting_power: Uint128,
615) -> Result<Response<NeutronMsg>, ContractError> {
616 if let Some(user_info) = USER_INFO.may_load(store, voter)? {
617 let block_ts = env.block.time.seconds();
618
619 let whitelist: HashSet<_> = POOLS_WHITELIST.load(store)?.into_iter().collect();
620 user_info
621 .votes
622 .iter()
623 .filter(|(pool, _)| whitelist.contains(pool.as_str()))
624 .try_for_each(|(pool, weight)| {
625 let pool_info = VOTED_POOLS.load(store, pool)?;
626 let pool_dedicated_vp = if pool_info.init_ts <= user_info.vote_ts {
628 user_info
629 .voting_power
630 .multiply_ratio(weight.numerator(), weight.denominator())
631 } else {
632 Uint128::zero()
633 };
634 let add_vp =
635 new_voting_power.multiply_ratio(weight.numerator(), weight.denominator());
636
637 let new_pool_info = pool_info.with_sub_vp(pool_dedicated_vp).with_add_vp(add_vp);
638 VOTED_POOLS.save(store, pool, &new_pool_info, block_ts)
639 })?;
640
641 USER_INFO.save(
643 store,
644 voter,
645 &UserInfo {
646 voting_power: new_voting_power,
647 ..user_info
648 },
649 block_ts,
650 )?;
651
652 Ok(Response::default().add_attributes([
653 attr("action", "update_user_votes"),
654 attr("voter", voter),
655 attr("old_voting_power", user_info.voting_power),
656 attr("new_voting_power", new_voting_power),
657 ]))
658 } else {
659 Ok(Response::default())
660 }
661}
662
663pub fn tune_pools(
671 deps: DepsMut<NeutronQuery>,
672 env: Env,
673) -> Result<Response<NeutronMsg>, ContractError> {
674 let tune_info = TUNE_INFO.load(deps.storage)?;
675 let block_ts = env.block.time.seconds();
676
677 ensure!(
678 tune_info.tune_ts + EPOCH_LENGTH <= block_ts,
679 ContractError::TuneCooldown(tune_info.tune_ts + EPOCH_LENGTH)
680 );
681
682 let config = CONFIG.load(deps.storage)?;
683 let ibc_fee = min_ntrn_ibc_fee(deps.as_ref())?;
684 let deps = deps.into_empty();
685
686 let voted_pools = VOTED_POOLS
687 .keys(deps.storage, None, None, Order::Ascending)
688 .collect::<StdResult<HashSet<_>>>()?;
689 let outposts = get_active_outposts(deps.storage)?;
690 let epoch_start = get_epoch_start(block_ts);
691
692 let TuneResult {
693 candidates,
694 new_emissions_state,
695 next_pools_grouped,
696 } = simulate_tune(deps.as_ref(), &voted_pools, &outposts, epoch_start, &config)?;
697
698 let total_pool_limit = config.pools_per_outpost as usize * outposts.len();
699
700 if candidates.len() > total_pool_limit {
705 let total_vp = candidates
706 .iter()
707 .fold(Uint128::zero(), |acc, (_, (_, vp))| acc + vp);
708
709 let new_whitelist: HashSet<_> = candidates
710 .iter()
711 .skip(total_pool_limit)
712 .filter(|(_, (_, pool_vp))| {
713 let threshold_vp = total_vp.multiply_ratio(
714 config.whitelist_threshold.numerator(),
715 config.whitelist_threshold.denominator(),
716 );
717 *pool_vp >= threshold_vp
718 })
719 .chain(candidates.iter().take(total_pool_limit))
720 .map(|(_, (pool, _))| (*pool).clone())
721 .collect();
722
723 voted_pools
725 .difference(&new_whitelist)
726 .try_for_each(|pool| VOTED_POOLS.remove(deps.storage, pool, block_ts))?;
727
728 POOLS_WHITELIST.save(deps.storage, &new_whitelist.into_iter().collect())?;
729 }
730
731 let mut attrs = vec![attr("action", "tune_pools")];
732 let mut outpost_emissions_statuses = HashMap::new();
733 let setup_pools_msgs = next_pools_grouped
734 .iter()
735 .map(|(prefix, raw_schedules)| {
736 let outpost_info = outposts.get(prefix).unwrap();
737
738 let (schedules, astro_funds) = raw_emissions_to_schedules(
739 &env,
740 raw_schedules,
741 &outpost_info.astro_denom,
742 &config.astro_denom,
743 );
744
745 let msg = if let Some(params) = &outpost_info.params {
746 outpost_emissions_statuses.insert(prefix.clone(), OutpostStatus::InProgress);
747 build_emission_ibc_msg(&env, params, &ibc_fee, astro_funds, &schedules)?
748 } else {
749 let incentives_msg = incentives::ExecuteMsg::IncentivizeMany(schedules);
750 wasm_execute(&config.incentives_addr, &incentives_msg, vec![astro_funds])?.into()
751 };
752
753 attrs.push(attr("outpost", prefix));
754 attrs.push(attr(
755 "pools",
756 serde_json::to_string(&raw_schedules)
757 .map_err(|err| StdError::generic_err(err.to_string()))?,
758 ));
759
760 Ok(msg)
761 })
762 .collect::<StdResult<Vec<CosmosMsg<NeutronMsg>>>>()?;
763
764 TUNE_INFO.save(
765 deps.storage,
766 &TuneInfo {
767 tune_ts: epoch_start,
768 pools_grouped: next_pools_grouped,
769 outpost_emissions_statuses,
770 emissions_state: new_emissions_state,
771 },
772 block_ts,
773 )?;
774
775 Ok(Response::new()
776 .add_messages(setup_pools_msgs)
777 .add_attributes(attrs))
778}
779
780pub fn update_config(
783 deps: DepsMut<NeutronQuery>,
784 info: MessageInfo,
785 pools_limit: Option<u64>,
786 whitelisting_fee: Option<Coin>,
787 fee_receiver: Option<String>,
788 emissions_multiple: Option<Decimal>,
789 max_astro: Option<Uint128>,
790) -> Result<Response<NeutronMsg>, ContractError> {
791 nonpayable(&info)?;
792 let mut config = CONFIG.load(deps.storage)?;
793
794 ensure!(info.sender == config.owner, ContractError::Unauthorized {});
795
796 let mut attrs = vec![attr("action", "update_config")];
797
798 if let Some(pools_limit) = pools_limit {
799 attrs.push(attr("new_pools_limit", pools_limit.to_string()));
800 config.pools_per_outpost = pools_limit;
801 }
802
803 if let Some(whitelisting_fee) = whitelisting_fee {
804 attrs.push(attr("new_whitelisting_fee", whitelisting_fee.to_string()));
805 config.whitelisting_fee = whitelisting_fee;
806 }
807
808 if let Some(fee_receiver) = fee_receiver {
809 attrs.push(attr("new_fee_receiver", &fee_receiver));
810 config.fee_receiver = deps.api.addr_validate(&fee_receiver)?;
811 }
812
813 if let Some(emissions_multiple) = emissions_multiple {
814 attrs.push(attr(
815 "new_emissions_multiple",
816 emissions_multiple.to_string(),
817 ));
818 config.emissions_multiple = emissions_multiple;
819 }
820
821 if let Some(max_astro) = max_astro {
822 attrs.push(attr("new_max_astro", max_astro.to_string()));
823 config.max_astro = max_astro;
824 }
825
826 config.validate()?;
827
828 CONFIG.save(deps.storage, &config)?;
829
830 Ok(Response::default().add_attributes(attrs))
831}
832
833pub fn register_proposal(
836 deps: DepsMut<NeutronQuery>,
837 env: Env,
838 proposal_id: u64,
839) -> Result<Response<NeutronMsg>, ContractError> {
840 let config = CONFIG.load(deps.storage)?;
841 let proposal = deps
843 .querier
844 .query_wasm_smart::<assembly::Proposal>(
845 &config.assembly,
846 &assembly::QueryMsg::Proposal { proposal_id },
847 )
848 .and_then(|proposal| {
849 ensure!(
850 env.block.height <= proposal.end_block,
851 StdError::generic_err("Proposal is not active")
852 );
853
854 Ok(proposal)
855 })?;
856
857 let outposts = get_active_outposts(deps.storage)?;
858
859 let data = to_json_binary(&VxAstroIbcMsg::RegisterProposal {
860 proposal_id,
861 start_time: proposal.start_time,
862 })?;
863 let timeout = IbcTimeout::from(env.block.time.plus_seconds(IBC_TIMEOUT));
864
865 let mut attrs = vec![("action", "register_proposal")];
866
867 let ibc_messages: Vec<CosmosMsg<NeutronMsg>> = outposts
868 .iter()
869 .filter_map(|(outpost, outpost_info)| {
870 outpost_info.params.as_ref().map(|params| {
871 attrs.push(("outpost", outpost));
872 IbcMsg::SendPacket {
873 channel_id: params.voting_channel.clone(),
874 data: data.clone(),
875 timeout: timeout.clone(),
876 }
877 .into()
878 })
879 })
880 .collect();
881
882 Ok(Response::new()
883 .add_messages(ibc_messages)
884 .add_attributes(attrs))
885}