astroport_emissions_controller/
execute.rs

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/// Exposes all the execute functions available in the contract.
41#[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
180/// Permissionless endpoint to whitelist a pool.
181/// Requires a fee to be paid.
182/// This endpoint is meant to be executed by users from the Hub or from other outposts via IBC hooks.
183pub 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 that LP token is not blacklisted
198    ensure!(
199        !POOLS_BLACKLIST.has(deps.storage, &pool),
200        ContractError::PoolIsBlacklisted(pool.clone())
201    );
202
203    // Perform basic LP token validation. Ensure the outpost exists.
204    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            // Validate LP token on the Hub
208            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    // Astro pools receive flat emissions hence we don't allow people to vote for them
216    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    // Starting the voting process from scratch for this pool
236    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    // Checking for duplicates
268    ensure!(
269        remove.iter().chain(add.iter()).all_unique(),
270        StdError::generic_err("Duplicated LP tokens found")
271    );
272
273    // Remove pools from blacklist
274    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    // Add pools to blacklist
284    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        // If key doesn't exist .remove() doesn't throw an error
291        VOTED_POOLS.remove(deps.storage, lp_token, env.block.time.seconds())?;
292        POOLS_BLACKLIST.save(deps.storage, lp_token, &())?;
293    }
294
295    // And remove pools from the whitelist if they are there
296    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
313/// Permissioned endpoint to add or update outpost.
314/// Performs several simple checks to cut off possible human errors.
315pub 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        // Remove this pool from whitelist
339        POOLS_WHITELIST.update::<_, StdError>(deps.storage, |mut pools| {
340            pools.retain(|pool| pool != &conf.astro_pool);
341            Ok(pools)
342        })?;
343
344        // And remove from votable pools
345        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(&params.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            &params.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                            &params.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
406/// Jails outpost as well as removes all whitelisted
407/// and being voted pools related to this outpost.
408pub 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
448/// This permissionless endpoint retries failed emission IBC messages.
449pub 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                    // Ignoring this outpost if it failed to serialize IbcHook msg for some reason
477                    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
504/// The function checks that:
505/// * user didn't vote at the current epoch,
506/// * sum of all percentage values <= 1.
507/// User can direct his voting power partially.
508///
509/// The function cancels changes applied by previous votes and applies new votes for the next epoch.
510/// New vote parameters are saved in [`USER_INFO`].
511///
512/// * **voter** is a voter address.
513/// * **voting_power** is a user's voting power reported from the outpost.
514/// * **votes** is a map LP token -> percentage of user's voting power to direct to this pool.
515pub 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    // User can vote once per epoch
527    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    // Cancel previous user votes. Filter non-whitelisted pools.
549    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            // Subtract old vote from pool voting power if pool wasn't reset to 0
556            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    // Apply new votes with fresh user voting power.
568    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    // Save pool changes which are not part of the new votes
589    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
608/// This function updates existing user's voting power contribution in pool votes.
609/// Is used to reflect user's vxASTRO balance changes in the emissions controller contract.
610pub 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                // Subtract old vote from pool voting power if pool wasn't reset to 0
627                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        // Updating only voting power
642        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
663/// The function checks that the last pools tuning happened >= 14 days ago.
664/// Then it calculates voting power per each pool.
665/// takes top X pools by voting power, where X is
666/// 'config.pools_per_outpost' * number of outposts,
667/// calculates total ASTRO emission amount for upcoming epoch,
668/// distributes it between selected pools
669/// and sends emission messages to each outpost.
670pub 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 list size is more than the total pool number limit,
701    // we need to whitelist all candidates
702    // and those which have more than the threshold voting power.
703    // Otherwise, keep the current whitelist.
704    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        // Remove all non-whitelisted pools
724        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
780/// Permissioned to the contract owner.
781/// Updates the contract configuration.
782pub 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
833/// Register an active proposal on all available outposts.
834/// Endpoint is permissionless so anyone can retry to register a proposal in case of IBC timeout.
835pub 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    // Ensure a proposal exists and active
842    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}