From ec2ed53c47fadf84fe9d4c1a67bd2c73b15022c7 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:14:07 +0400 Subject: [PATCH 1/4] feat(em. controller): jail outpost instead of removing from state --- contracts/emissions_controller/src/error.rs | 6 + contracts/emissions_controller/src/execute.rs | 67 +++++++--- contracts/emissions_controller/src/ibc.rs | 120 ++++++++++-------- contracts/emissions_controller/src/query.rs | 15 +-- contracts/emissions_controller/src/state.rs | 17 +++ contracts/emissions_controller/src/utils.rs | 11 +- .../tests/emissions_controller_integration.rs | 97 +++++++++++++- .../src/emissions_controller/hub.rs | 13 +- .../astroport-emissions-controller.json | 35 ++++- .../raw/execute.json | 28 +++- .../raw/response_to_list_outposts.json | 7 +- 11 files changed, 318 insertions(+), 98 deletions(-) diff --git a/contracts/emissions_controller/src/error.rs b/contracts/emissions_controller/src/error.rs index 4a4993a..6dfa432 100644 --- a/contracts/emissions_controller/src/error.rs +++ b/contracts/emissions_controller/src/error.rs @@ -77,4 +77,10 @@ pub enum ContractError { #[error("Failed to migrate contract")] MigrationError {}, + + #[error("Outpost {outpost} not found")] + OutpostNotFound { outpost: String }, + + #[error("Outpost {outpost} is jailed. Only vxASTRO unlocks are available")] + JailedOutpost { outpost: String }, } diff --git a/contracts/emissions_controller/src/execute.rs b/contracts/emissions_controller/src/execute.rs index 1b088d7..82fa79b 100644 --- a/contracts/emissions_controller/src/execute.rs +++ b/contracts/emissions_controller/src/execute.rs @@ -27,7 +27,8 @@ use astroport_governance::{assembly, voting_escrow}; use crate::error::ContractError; use crate::state::{ - CONFIG, OUTPOSTS, OWNERSHIP_PROPOSAL, POOLS_WHITELIST, TUNE_INFO, USER_INFO, VOTED_POOLS, + get_active_outposts, CONFIG, OUTPOSTS, OWNERSHIP_PROPOSAL, POOLS_WHITELIST, TUNE_INFO, + USER_INFO, VOTED_POOLS, }; use crate::utils::{ build_emission_ibc_msg, determine_outpost_prefix, get_epoch_start, get_outpost_prefix, @@ -148,7 +149,8 @@ pub fn execute( outpost_params, astro_pool_config, ), - HubMsg::RemoveOutpost { prefix } => remove_outpost(deps, env, info, prefix), + HubMsg::JailOutpost { prefix } => jail_outpost(deps, env, info, prefix), + HubMsg::UnjailOutpost { prefix } => unjail_outpost(deps, info, prefix), HubMsg::TunePools {} => tune_pools(deps, env), HubMsg::RetryFailedOutposts {} => retry_failed_outposts(deps, info, env), HubMsg::UpdateConfig { @@ -189,9 +191,7 @@ pub fn whitelist_pool( ); // Perform basic LP token validation. Ensure the outpost exists. - let outposts = OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; + let outposts = get_active_outposts(deps.storage)?; if let Some(prefix) = get_outpost_prefix(&pool, &outposts) { if outposts.get(&prefix).unwrap().params.is_none() { // Validate LP token on the Hub @@ -311,15 +311,16 @@ pub fn update_outpost( params: outpost_params, astro_denom, astro_pool_config, + jailed: false, }, )?; Ok(Response::default().add_attributes([("action", "update_outpost"), ("prefix", &prefix)])) } -/// Removes outpost from the contract as well as all whitelisted +/// Jails outpost as well as removes all whitelisted /// and being voted pools related to this outpost. -pub fn remove_outpost( +pub fn jail_outpost( deps: DepsMut, env: Env, info: MessageInfo, @@ -345,9 +346,45 @@ pub fn remove_outpost( Ok(whitelist) })?; - OUTPOSTS.remove(deps.storage, &prefix); + OUTPOSTS.update(deps.storage, &prefix, |outpost| { + if let Some(outpost) = outpost { + Ok(OutpostInfo { + jailed: true, + ..outpost + }) + } else { + Err(ContractError::OutpostNotFound { + outpost: prefix.clone(), + }) + } + })?; - Ok(Response::default().add_attributes([("action", "remove_outpost"), ("prefix", &prefix)])) + Ok(Response::default().add_attributes([("action", "jail_outpost"), ("prefix", &prefix)])) +} + +pub fn unjail_outpost( + deps: DepsMut, + info: MessageInfo, + prefix: String, +) -> Result, ContractError> { + nonpayable(&info)?; + let config = CONFIG.load(deps.storage)?; + ensure!(info.sender == config.owner, ContractError::Unauthorized {}); + + OUTPOSTS.update(deps.storage, &prefix, |outpost| { + if let Some(outpost) = outpost { + Ok(OutpostInfo { + jailed: false, + ..outpost + }) + } else { + Err(ContractError::OutpostNotFound { + outpost: prefix.clone(), + }) + } + })?; + + Ok(Response::default().add_attributes([("action", "unjail_outpost"), ("prefix", &prefix)])) } /// This permissionless endpoint retries failed emission IBC messages. @@ -358,9 +395,7 @@ pub fn retry_failed_outposts( ) -> Result, ContractError> { nonpayable(&info)?; let mut tune_info = TUNE_INFO.load(deps.storage)?; - let outposts = OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; + let outposts = get_active_outposts(deps.storage)?; let mut attrs = vec![attr("action", "retry_failed_outposts")]; let ibc_fee = min_ntrn_ibc_fee(deps.as_ref())?; @@ -593,9 +628,7 @@ pub fn tune_pools( let voted_pools = VOTED_POOLS .keys(deps.storage, None, None, Order::Ascending) .collect::>>()?; - let outposts = OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; + let outposts = get_active_outposts(deps.storage)?; let epoch_start = get_epoch_start(block_ts); let TuneResult { @@ -763,9 +796,7 @@ pub fn register_proposal( Ok(proposal) })?; - let outposts = OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; + let outposts = get_active_outposts(deps.storage)?; let data = to_json_binary(&VxAstroIbcMsg::RegisterProposal { proposal_id, diff --git a/contracts/emissions_controller/src/ibc.rs b/contracts/emissions_controller/src/ibc.rs index 36c4fc6..8dd064c 100644 --- a/contracts/emissions_controller/src/ibc.rs +++ b/contracts/emissions_controller/src/ibc.rs @@ -3,8 +3,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ ensure, from_json, wasm_execute, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcPacketAckMsg, - IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, Order, StdError, - StdResult, + IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, StdError, StdResult, }; use astroport_governance::assembly; @@ -15,7 +14,7 @@ use astroport_governance::emissions_controller::msg::{ use crate::error::ContractError; use crate::execute::{handle_update_user, handle_vote}; -use crate::state::{CONFIG, OUTPOSTS}; +use crate::state::{get_all_outposts, CONFIG}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn ibc_channel_open( @@ -85,14 +84,13 @@ pub fn do_packet_receive( env: Env, msg: IbcPacketReceiveMsg, ) -> Result { - // Ensure this outpost is registered - OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .find_map(|data| { - let (outpost_prefix, outpost) = data.ok()?; + // Ensure this outpost was ever registered + let (prefix, outpost) = get_all_outposts(deps.storage)? + .into_iter() + .find_map(|(prefix, outpost)| { outpost.params.as_ref().and_then(|params| { if msg.packet.dest.channel_id == params.voting_channel { - Some(outpost_prefix.clone()) + Some((prefix.clone(), outpost.clone())) } else { None } @@ -105,51 +103,70 @@ pub fn do_packet_receive( )) })?; - match from_json(&msg.packet.data)? { - VxAstroIbcMsg::EmissionsVote { - voter, - voting_power, - votes, - } => handle_vote(deps, env, &voter, voting_power, votes).map(|orig_response| { - IbcReceiveResponse::new() - .add_attributes(orig_response.attributes) - .set_ack(ack_ok()) - }), - VxAstroIbcMsg::UpdateUserVotes { - voter, - voting_power, - .. - } => handle_update_user(deps.storage, env, voter.as_str(), voting_power).map( - |orig_response| { + let ibc_msg: VxAstroIbcMsg = from_json(&msg.packet.data)?; + + if outpost.jailed { + match ibc_msg { + VxAstroIbcMsg::UpdateUserVotes { + voter, + voting_power, + is_unlock: true, + } => handle_update_user(deps.storage, env, voter.as_str(), voting_power).map( + |orig_response| { + IbcReceiveResponse::new() + .add_attributes(orig_response.attributes) + .set_ack(ack_ok()) + }, + ), + _ => Err(ContractError::JailedOutpost { outpost: prefix }), + } + } else { + match ibc_msg { + VxAstroIbcMsg::EmissionsVote { + voter, + voting_power, + votes, + } => handle_vote(deps, env, &voter, voting_power, votes).map(|orig_response| { IbcReceiveResponse::new() .add_attributes(orig_response.attributes) .set_ack(ack_ok()) - }, - ), - VxAstroIbcMsg::GovernanceVote { - voter, - voting_power, - proposal_id, - vote, - } => { - let config = CONFIG.load(deps.storage)?; - let cast_vote_msg = wasm_execute( - config.assembly, - &assembly::ExecuteMsg::CastVoteOutpost { - voter, - voting_power, - proposal_id, - vote, + }), + VxAstroIbcMsg::UpdateUserVotes { + voter, + voting_power, + .. + } => handle_update_user(deps.storage, env, voter.as_str(), voting_power).map( + |orig_response| { + IbcReceiveResponse::new() + .add_attributes(orig_response.attributes) + .set_ack(ack_ok()) }, - vec![], - )?; - - Ok(IbcReceiveResponse::new() - .add_message(cast_vote_msg) - .set_ack(ack_ok())) - } - VxAstroIbcMsg::RegisterProposal { .. } => { - unreachable!("Hub can't receive RegisterProposal message") + ), + VxAstroIbcMsg::GovernanceVote { + voter, + voting_power, + proposal_id, + vote, + } => { + let config = CONFIG.load(deps.storage)?; + let cast_vote_msg = wasm_execute( + config.assembly, + &assembly::ExecuteMsg::CastVoteOutpost { + voter, + voting_power, + proposal_id, + vote, + }, + vec![], + )?; + + Ok(IbcReceiveResponse::new() + .add_message(cast_vote_msg) + .set_ack(ack_ok())) + } + VxAstroIbcMsg::RegisterProposal { .. } => { + unreachable!("Hub can't receive RegisterProposal message") + } } } } @@ -206,7 +223,7 @@ mod unit_tests { }; use astroport_governance::emissions_controller::msg::IbcAckResult; - use crate::state::{POOLS_WHITELIST, VOTED_POOLS}; + use crate::state::{OUTPOSTS, POOLS_WHITELIST, VOTED_POOLS}; use super::*; @@ -380,6 +397,7 @@ mod unit_tests { }), astro_denom: "".to_string(), astro_pool_config: None, + jailed: false, }, ) .unwrap(); diff --git a/contracts/emissions_controller/src/query.rs b/contracts/emissions_controller/src/query.rs index e334abc..bb01b76 100644 --- a/contracts/emissions_controller/src/query.rs +++ b/contracts/emissions_controller/src/query.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -13,7 +13,10 @@ use astroport_governance::emissions_controller::hub::{ }; use crate::error::ContractError; -use crate::state::{CONFIG, OUTPOSTS, POOLS_WHITELIST, TUNE_INFO, USER_INFO, VOTED_POOLS}; +use crate::state::{ + get_active_outposts, get_all_outposts, CONFIG, POOLS_WHITELIST, TUNE_INFO, USER_INFO, + VOTED_POOLS, +}; use crate::utils::simulate_tune; /// Expose available contract queries. @@ -93,9 +96,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { - let outposts = OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; + let outposts = get_all_outposts(deps.storage)?.into_iter().collect_vec(); Ok(to_json_binary(&outposts)?) } QueryMsg::QueryWhitelist { limit, start_after } => { @@ -127,9 +128,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result>>()?; - let outposts = OUTPOSTS - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; + let outposts = get_active_outposts(deps.storage)?; let config = CONFIG.load(deps.storage)?; let tune_result = simulate_tune( diff --git a/contracts/emissions_controller/src/state.rs b/contracts/emissions_controller/src/state.rs index 2375fc9..28b2a5a 100644 --- a/contracts/emissions_controller/src/state.rs +++ b/contracts/emissions_controller/src/state.rs @@ -1,5 +1,7 @@ use astroport::common::OwnershipProposal; +use cosmwasm_std::{Order, StdResult, Storage}; use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; +use std::collections::HashMap; use astroport_governance::emissions_controller::hub::{ Config, OutpostInfo, TuneInfo, UserInfo, VotedPoolInfo, @@ -34,3 +36,18 @@ pub const TUNE_INFO: SnapshotItem = SnapshotItem::new( "tune_info__changelog", Strategy::EveryBlock, ); + +pub fn get_all_outposts(storage: &dyn Storage) -> StdResult> { + OUTPOSTS + .range(storage, None, None, Order::Ascending) + .collect() +} + +pub fn get_active_outposts(storage: &dyn Storage) -> StdResult> { + get_all_outposts(storage).map(|outposts| { + outposts + .into_iter() + .filter(|(_, outpost)| !outpost.jailed) + .collect() + }) +} diff --git a/contracts/emissions_controller/src/utils.rs b/contracts/emissions_controller/src/utils.rs index 48fd064..09462b9 100644 --- a/contracts/emissions_controller/src/utils.rs +++ b/contracts/emissions_controller/src/utils.rs @@ -6,7 +6,7 @@ use astroport::incentives::{IncentivesSchedule, InputSchedule}; use cosmwasm_schema::cw_serde; use cosmwasm_schema::serde::Serialize; use cosmwasm_std::{ - coin, Coin, CosmosMsg, Decimal, Deps, Env, Order, QuerierWrapper, StdError, StdResult, Storage, + coin, Coin, CosmosMsg, Decimal, Deps, Env, QuerierWrapper, StdError, StdResult, Storage, Uint128, }; use itertools::Itertools; @@ -25,7 +25,7 @@ use astroport_governance::emissions_controller::outpost::OutpostMsg; use astroport_governance::emissions_controller::utils::check_lp_token; use crate::error::ContractError; -use crate::state::{OUTPOSTS, TUNE_INFO, VOTED_POOLS}; +use crate::state::{get_active_outposts, TUNE_INFO, VOTED_POOLS}; /// Determine outpost prefix from address or tokenfactory denom. pub fn determine_outpost_prefix(value: &str) -> Option { @@ -81,10 +81,9 @@ pub fn get_outpost_from_hub_channel( source_channel: String, get_channel_closure: impl Fn(&OutpostParams) -> &String, ) -> StdResult { - OUTPOSTS - .range(store, None, None, Order::Ascending) - .find_map(|data| { - let (outpost_prefix, outpost) = data.ok()?; + get_active_outposts(store)? + .into_iter() + .find_map(|(outpost_prefix, outpost)| { outpost.params.as_ref().and_then(|params| { if get_channel_closure(params).eq(&source_channel) { Some(outpost_prefix.clone()) diff --git a/contracts/emissions_controller/tests/emissions_controller_integration.rs b/contracts/emissions_controller/tests/emissions_controller_integration.rs index 93dd445..a80a4fd 100644 --- a/contracts/emissions_controller/tests/emissions_controller_integration.rs +++ b/contracts/emissions_controller/tests/emissions_controller_integration.rs @@ -47,6 +47,7 @@ pub fn voting_test() { astro_denom: helper.astro.clone(), params: None, astro_pool_config: None, + jailed: false, }; helper.add_outpost("neutron", neutron).unwrap(); @@ -173,6 +174,7 @@ fn test_whitelist() { astro_pool: astro_pool.clone(), constant_emissions: Uint128::one(), }), + jailed: false, }; helper.add_outpost("neutron", neutron).unwrap(); @@ -235,6 +237,7 @@ fn test_outpost_management() { astro_pool: "wasm1pool".to_string(), constant_emissions: Uint128::one(), }), + jailed: false, }; let err = helper @@ -306,6 +309,7 @@ fn test_outpost_management() { ics20_channel: "channel-2".to_string(), }), astro_pool_config: None, + jailed: false, }; let err = helper.add_outpost("osmo", osmosis.clone()).unwrap_err(); @@ -345,7 +349,7 @@ fn test_outpost_management() { osmosis.params.as_mut().unwrap().emissions_controller = "osmo1controller".to_string(); helper.add_outpost("osmo", osmosis.clone()).unwrap(); - let outposts = helper + let mut outposts = helper .app .wrap() .query_wasm_smart::>( @@ -353,10 +357,11 @@ fn test_outpost_management() { &emissions_controller::hub::QueryMsg::ListOutposts {}, ) .unwrap(); + outposts.sort_by(|a, b| a.0.cmp(&b.0)); assert_eq!( outposts, vec![ - ("neutron".to_string(), neutron), + ("neutron".to_string(), neutron.clone()), ("osmo".to_string(), osmosis.clone()) ] ); @@ -411,14 +416,14 @@ fn test_outpost_management() { let whitelist = helper.query_whitelist().unwrap(); assert_eq!(whitelist, vec![lp_token.to_string()]); - // Remove neutron outpost + // Jail neutron outpost let rand_user = helper.app.api().addr_make("random"); let err = helper .app .execute_contract( rand_user, helper.emission_controller.clone(), - &ExecuteMsg::Custom(HubMsg::RemoveOutpost { + &ExecuteMsg::Custom(HubMsg::JailOutpost { prefix: "neutron".to_string(), }), &[], @@ -434,7 +439,7 @@ fn test_outpost_management() { .execute_contract( helper.owner.clone(), helper.emission_controller.clone(), - &ExecuteMsg::Custom(HubMsg::RemoveOutpost { + &ExecuteMsg::Custom(HubMsg::JailOutpost { prefix: "neutron".to_string(), }), &[], @@ -452,9 +457,85 @@ fn test_outpost_management() { ContractError::PoolIsNotWhitelisted(lp_token.to_string()) ); + // Cant whitelist pool belonging to jailed outpost + helper + .mint_tokens(&user, &[helper.whitelisting_fee.clone()]) + .unwrap(); + let err = helper + .whitelist(&user, &lp_token, &[helper.whitelisting_fee.clone()]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoOutpostForPool(lp_token.to_string()) + ); + // Ensure neutron pool was removed from votable pools let voted_pools = helper.query_pools_vp(None).unwrap(); assert_eq!(voted_pools, vec![]); + + // Neutron outpost still exists in the state + let mut outposts = helper + .app + .wrap() + .query_wasm_smart::>( + helper.emission_controller.clone(), + &emissions_controller::hub::QueryMsg::ListOutposts {}, + ) + .unwrap(); + outposts.sort_by(|a, b| a.0.cmp(&b.0)); + assert_eq!( + outposts, + [ + ( + "neutron".to_string(), + OutpostInfo { + jailed: true, + ..neutron.clone() + } + ), + ("osmo".to_string(), osmosis.clone()), + ] + ); + + // Unjail Neutron + helper + .app + .execute_contract( + helper.owner.clone(), + helper.emission_controller.clone(), + &ExecuteMsg::Custom(HubMsg::UnjailOutpost { + prefix: "neutron".to_string(), + }), + &[], + ) + .unwrap(); + + let mut outposts = helper + .app + .wrap() + .query_wasm_smart::>( + helper.emission_controller.clone(), + &emissions_controller::hub::QueryMsg::ListOutposts {}, + ) + .unwrap(); + outposts.sort_by(|a, b| a.0.cmp(&b.0)); + assert_eq!( + outposts, + [ + ("neutron".to_string(), neutron), + ("osmo".to_string(), osmosis.clone()), + ] + ); + + // Confirm we can whitelist neutron pool + helper + .mint_tokens(&user, &[helper.whitelisting_fee.clone()]) + .unwrap(); + helper + .whitelist(&user, &lp_token, &[helper.whitelisting_fee.clone()]) + .unwrap(); + let whitelist = helper.query_whitelist().unwrap(); + assert_eq!(whitelist, vec![lp_token.to_string()]); } #[test] @@ -483,6 +564,7 @@ fn test_tune_only_hub() { astro_pool: astro_pool.clone(), constant_emissions: 1_000_000_000u128.into(), }), + jailed: false, }; helper.add_outpost("neutron", neutron.clone()).unwrap(); @@ -705,6 +787,7 @@ fn test_tune_outpost() { astro_pool: astro_pool.to_string(), constant_emissions: 1_000_000_000u128.into(), }), + jailed: false, }; helper.add_outpost("osmo", osmosis.clone()).unwrap(); @@ -932,6 +1015,7 @@ fn test_lock_unlock_vxastro() { astro_denom: helper.astro.clone(), params: None, astro_pool_config: None, + jailed: false, }, ) .unwrap(); @@ -1093,6 +1177,7 @@ fn test_instant_unlock_vxastro() { astro_denom: helper.astro.clone(), params: None, astro_pool_config: None, + jailed: false, }, ) .unwrap(); @@ -1213,6 +1298,7 @@ fn test_some_epochs() { ics20_channel: "channel-2".to_string(), }), astro_pool_config: None, + jailed: false, }, ) .unwrap(); @@ -1380,6 +1466,7 @@ fn test_interchain_governance() { ics20_channel: "channel-2".to_string(), }), astro_pool_config: None, + jailed: false, }, ) .unwrap(); diff --git a/packages/astroport-governance/src/emissions_controller/hub.rs b/packages/astroport-governance/src/emissions_controller/hub.rs index 9d52aec..6ca8865 100644 --- a/packages/astroport-governance/src/emissions_controller/hub.rs +++ b/packages/astroport-governance/src/emissions_controller/hub.rs @@ -79,8 +79,14 @@ pub enum HubMsg { /// A pool that must receive flat ASTRO emissions. Optional. astro_pool_config: Option, }, - /// Remove an outpost - RemoveOutpost { prefix: String }, + /// Jail an outpost. + /// Jailed outposts can't participate in the voting process but still allow + /// outpost users to unlock their vxASTRO. + JailOutpost { prefix: String }, + /// Unjail an outpost. + /// Unjailed outposts retain all previous configurations but will need to whitelist pools and + /// start a voting process from scratch. + UnjailOutpost { prefix: String }, /// Permissionless endpoint to stream proposal info from the Hub to all outposts RegisterProposal { proposal_id: u64 }, } @@ -240,6 +246,9 @@ pub struct OutpostInfo { pub astro_denom: String, /// A pool that must receive flat ASTRO emissions. Optional. pub astro_pool_config: Option, + /// Defines whether outpost is jailed. Jailed outposts can't participate in the voting process + /// but they still allow remote users to unstake their vxASTRO. + pub jailed: bool, } #[cw_serde] diff --git a/schemas/astroport-emissions-controller/astroport-emissions-controller.json b/schemas/astroport-emissions-controller/astroport-emissions-controller.json index 6e9a28b..9e144fe 100644 --- a/schemas/astroport-emissions-controller/astroport-emissions-controller.json +++ b/schemas/astroport-emissions-controller/astroport-emissions-controller.json @@ -607,13 +607,35 @@ "additionalProperties": false }, { - "description": "Remove an outpost", + "description": "Jail an outpost. Jailed outposts can't participate in the voting process but still allow outpost users to unlock their vxASTRO.", "type": "object", "required": [ - "remove_outpost" + "jail_outpost" ], "properties": { - "remove_outpost": { + "jail_outpost": { + "type": "object", + "required": [ + "prefix" + ], + "properties": { + "prefix": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Unjail an outpost. Unjailed outposts retain all previous configurations but will need to whitelist pools and start a voting process from scratch.", + "type": "object", + "required": [ + "unjail_outpost" + ], + "properties": { + "unjail_outpost": { "type": "object", "required": [ "prefix" @@ -1079,7 +1101,8 @@ "OutpostInfo": { "type": "object", "required": [ - "astro_denom" + "astro_denom", + "jailed" ], "properties": { "astro_denom": { @@ -1097,6 +1120,10 @@ } ] }, + "jailed": { + "description": "Defines whether outpost is jailed. Jailed outposts can't participate in the voting process but they still allow remote users to unstake their vxASTRO.", + "type": "boolean" + }, "params": { "description": "Outpost params contain all necessary information to interact with the remote outpost. This field also serves as marker whether it is The hub (params: None) or remote outpost (Some(params))", "anyOf": [ diff --git a/schemas/astroport-emissions-controller/raw/execute.json b/schemas/astroport-emissions-controller/raw/execute.json index da973ce..773a591 100644 --- a/schemas/astroport-emissions-controller/raw/execute.json +++ b/schemas/astroport-emissions-controller/raw/execute.json @@ -355,13 +355,35 @@ "additionalProperties": false }, { - "description": "Remove an outpost", + "description": "Jail an outpost. Jailed outposts can't participate in the voting process but still allow outpost users to unlock their vxASTRO.", "type": "object", "required": [ - "remove_outpost" + "jail_outpost" ], "properties": { - "remove_outpost": { + "jail_outpost": { + "type": "object", + "required": [ + "prefix" + ], + "properties": { + "prefix": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Unjail an outpost. Unjailed outposts retain all previous configurations but will need to whitelist pools and start a voting process from scratch.", + "type": "object", + "required": [ + "unjail_outpost" + ], + "properties": { + "unjail_outpost": { "type": "object", "required": [ "prefix" diff --git a/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json b/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json index 6228d8b..a1926af 100644 --- a/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json +++ b/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json @@ -42,7 +42,8 @@ "OutpostInfo": { "type": "object", "required": [ - "astro_denom" + "astro_denom", + "jailed" ], "properties": { "astro_denom": { @@ -60,6 +61,10 @@ } ] }, + "jailed": { + "description": "Defines whether outpost is jailed. Jailed outposts can't participate in the voting process but they still allow remote users to unstake their vxASTRO.", + "type": "boolean" + }, "params": { "description": "Outpost params contain all necessary information to interact with the remote outpost. This field also serves as marker whether it is The hub (params: None) or remote outpost (Some(params))", "anyOf": [ From e056cbcde3493907b4ae9f232201497d7cfeaa2e Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:48:36 +0400 Subject: [PATCH 2/4] test ibc messages from jailed outpost --- contracts/emissions_controller/src/error.rs | 8 +- contracts/emissions_controller/src/execute.rs | 4 +- contracts/emissions_controller/src/ibc.rs | 94 ++++++++++++++++++- .../tests/emissions_controller_integration.rs | 52 ++++++++++ 4 files changed, 151 insertions(+), 7 deletions(-) diff --git a/contracts/emissions_controller/src/error.rs b/contracts/emissions_controller/src/error.rs index 6dfa432..d38d808 100644 --- a/contracts/emissions_controller/src/error.rs +++ b/contracts/emissions_controller/src/error.rs @@ -78,9 +78,9 @@ pub enum ContractError { #[error("Failed to migrate contract")] MigrationError {}, - #[error("Outpost {outpost} not found")] - OutpostNotFound { outpost: String }, + #[error("Outpost {prefix} not found")] + OutpostNotFound { prefix: String }, - #[error("Outpost {outpost} is jailed. Only vxASTRO unlocks are available")] - JailedOutpost { outpost: String }, + #[error("Outpost {prefix} is jailed. Only vxASTRO unlocks are available")] + JailedOutpost { prefix: String }, } diff --git a/contracts/emissions_controller/src/execute.rs b/contracts/emissions_controller/src/execute.rs index 82fa79b..af3c033 100644 --- a/contracts/emissions_controller/src/execute.rs +++ b/contracts/emissions_controller/src/execute.rs @@ -354,7 +354,7 @@ pub fn jail_outpost( }) } else { Err(ContractError::OutpostNotFound { - outpost: prefix.clone(), + prefix: prefix.clone(), }) } })?; @@ -379,7 +379,7 @@ pub fn unjail_outpost( }) } else { Err(ContractError::OutpostNotFound { - outpost: prefix.clone(), + prefix: prefix.clone(), }) } })?; diff --git a/contracts/emissions_controller/src/ibc.rs b/contracts/emissions_controller/src/ibc.rs index 8dd064c..5cbfaf0 100644 --- a/contracts/emissions_controller/src/ibc.rs +++ b/contracts/emissions_controller/src/ibc.rs @@ -118,7 +118,7 @@ pub fn do_packet_receive( .set_ack(ack_ok()) }, ), - _ => Err(ContractError::JailedOutpost { outpost: prefix }), + _ => Err(ContractError::JailedOutpost { prefix }), } } else { match ibc_msg { @@ -218,6 +218,7 @@ mod unit_tests { }; use neutron_sdk::bindings::query::NeutronQuery; + use astroport_governance::assembly::ProposalVoteOption; use astroport_governance::emissions_controller::hub::{ OutpostInfo, OutpostParams, VotedPoolInfo, }; @@ -481,4 +482,95 @@ mod unit_tests { let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap(); assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into())); } + + #[test] + fn test_jailed_outpost() { + let mut deps = mock_custom_dependencies(); + + // Mock jailed outpost + OUTPOSTS + .save( + deps.as_mut().storage, + "osmo", + &OutpostInfo { + params: Some(OutpostParams { + emissions_controller: "".to_string(), + voting_channel: "channel-2".to_string(), + ics20_channel: "".to_string(), + }), + astro_denom: "".to_string(), + astro_pool_config: None, + jailed: true, + }, + ) + .unwrap(); + + for (msg, is_error) in [ + ( + VxAstroIbcMsg::EmissionsVote { + voter: "osmo1voter".to_string(), + voting_power: 1000u128.into(), + votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]), + }, + true, + ), + ( + VxAstroIbcMsg::GovernanceVote { + voter: "osmo1voter".to_string(), + voting_power: 1000u128.into(), + proposal_id: 1, + vote: ProposalVoteOption::For, + }, + true, + ), + ( + VxAstroIbcMsg::UpdateUserVotes { + voter: "osmo1voter".to_string(), + voting_power: 2000u128.into(), + is_unlock: false, + }, + true, + ), + ( + VxAstroIbcMsg::UpdateUserVotes { + voter: "osmo1voter".to_string(), + voting_power: 0u128.into(), + is_unlock: true, + }, + false, + ), + ] { + let packet = IbcPacket::new( + to_json_binary(&msg).unwrap(), + IbcEndpoint { + port_id: "".to_string(), + channel_id: "".to_string(), + }, + IbcEndpoint { + port_id: "".to_string(), + channel_id: "channel-2".to_string(), + }, + 1, + IbcTimeout::with_timestamp(Timestamp::from_seconds(100)), + ); + let ibc_msg = IbcPacketReceiveMsg::new(packet, Addr::unchecked("doesnt matter")); + + let resp = ibc_packet_receive(deps.as_mut().into_empty(), mock_env(), ibc_msg).unwrap(); + let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap(); + + if is_error { + assert_eq!( + ack_err, + IbcAckResult::Error( + ContractError::JailedOutpost { + prefix: "osmo".to_string() + } + .to_string() + ) + ); + } else { + assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into())); + } + } + } } diff --git a/contracts/emissions_controller/tests/emissions_controller_integration.rs b/contracts/emissions_controller/tests/emissions_controller_integration.rs index a80a4fd..fba73f7 100644 --- a/contracts/emissions_controller/tests/emissions_controller_integration.rs +++ b/contracts/emissions_controller/tests/emissions_controller_integration.rs @@ -434,6 +434,24 @@ fn test_outpost_management() { ContractError::Unauthorized {} ); + let err = helper + .app + .execute_contract( + helper.owner.clone(), + helper.emission_controller.clone(), + &ExecuteMsg::Custom(HubMsg::JailOutpost { + prefix: "ntrn".to_string(), + }), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::OutpostNotFound { + prefix: "ntrn".to_string() + } + ); + helper .app .execute_contract( @@ -498,6 +516,40 @@ fn test_outpost_management() { ); // Unjail Neutron + let err = helper + .app + .execute_contract( + user.clone(), + helper.emission_controller.clone(), + &ExecuteMsg::Custom(HubMsg::UnjailOutpost { + prefix: "neutron".to_string(), + }), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + let err = helper + .app + .execute_contract( + helper.owner.clone(), + helper.emission_controller.clone(), + &ExecuteMsg::Custom(HubMsg::UnjailOutpost { + prefix: "ntrn".to_string(), + }), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::OutpostNotFound { + prefix: "ntrn".to_string() + } + ); + helper .app .execute_contract( From a9a659dd48ea7503b4bdccbd09776b49763975f5 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:10:01 +0400 Subject: [PATCH 3/4] bump versions and add migration --- Cargo.lock | 12 +++--- contracts/emissions_controller/Cargo.toml | 2 +- .../emissions_controller/src/migration.rs | 38 ++++++++++++++++++- packages/astroport-governance/Cargo.toml | 2 +- .../src/emissions_controller/hub.rs | 2 +- .../astroport-emissions-controller.json | 4 +- .../raw/response_to_list_outposts.json | 2 +- 7 files changed, 48 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b781f8..104a288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ version = "3.0.0" dependencies = [ "anyhow", "astroport 4.0.3", - "astroport-governance 4.1.0", + "astroport-governance 4.2.0", "astroport-staking", "astroport-tokenfactory-tracker", "astroport-voting-escrow", @@ -136,13 +136,13 @@ dependencies = [ [[package]] name = "astroport-emissions-controller" -version = "1.0.1" +version = "1.1.0" dependencies = [ "anyhow", "astro-assembly", "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "astroport-factory", - "astroport-governance 4.1.0", + "astroport-governance 4.2.0", "astroport-incentives", "astroport-pair", "astroport-staking", @@ -171,7 +171,7 @@ dependencies = [ "anyhow", "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "astroport-factory", - "astroport-governance 4.1.0", + "astroport-governance 4.2.0", "astroport-incentives", "astroport-pair", "astroport-voting-escrow", @@ -233,7 +233,7 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "4.1.0" +version = "4.2.0" dependencies = [ "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "cosmwasm-schema", @@ -320,7 +320,7 @@ name = "astroport-voting-escrow" version = "1.1.0" dependencies = [ "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", - "astroport-governance 4.1.0", + "astroport-governance 4.2.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 1.2.0", diff --git a/contracts/emissions_controller/Cargo.toml b/contracts/emissions_controller/Cargo.toml index 7bd80b3..2cad512 100644 --- a/contracts/emissions_controller/Cargo.toml +++ b/contracts/emissions_controller/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-emissions-controller" -version = "1.0.1" +version = "1.1.0" authors = ["Astroport"] edition = "2021" description = "Astroport vxASTRO Emissions Voting Contract" diff --git a/contracts/emissions_controller/src/migration.rs b/contracts/emissions_controller/src/migration.rs index 299bcfe..ecb8a8b 100644 --- a/contracts/emissions_controller/src/migration.rs +++ b/contracts/emissions_controller/src/migration.rs @@ -1,10 +1,25 @@ #![cfg(not(tarpaulin_include))] -use cosmwasm_std::{entry_point, DepsMut, Empty, Env, Response}; +use astroport_governance::emissions_controller::hub::{ + AstroPoolConfig, OutpostInfo, OutpostParams, +}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{entry_point, DepsMut, Empty, Env, Order, Response, StdResult}; use cw2::{get_contract_version, set_contract_version}; +use cw_storage_plus::Map; use crate::error::ContractError; use crate::instantiate::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::state::OUTPOSTS; + +#[cw_serde] +struct OldOutpostInfo { + pub params: Option, + pub astro_denom: String, + pub astro_pool_config: Option, +} + +const OLD_OUTPOSTS: Map<&str, OutpostInfo> = Map::new("outposts"); #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { @@ -12,7 +27,26 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result match contract_version.version.as_ref() { - "1.0.0" => Ok(()), + "1.0.0" | "1.0.1" => { + let old_outposts = OLD_OUTPOSTS + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + + for (prefix, old_outpost) in old_outposts { + OUTPOSTS.save( + deps.storage, + &prefix, + &OutpostInfo { + params: old_outpost.params, + astro_denom: old_outpost.astro_denom, + astro_pool_config: old_outpost.astro_pool_config, + jailed: false, + }, + )?; + } + + Ok(()) + } _ => Err(ContractError::MigrationError {}), }, _ => Err(ContractError::MigrationError {}), diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 0ef6c01..3bbbe55 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "4.1.0" +version = "4.2.0" authors = ["Astroport"] edition = "2021" description = "Astroport Governance common types, queriers and other utils" diff --git a/packages/astroport-governance/src/emissions_controller/hub.rs b/packages/astroport-governance/src/emissions_controller/hub.rs index 6ca8865..21908f5 100644 --- a/packages/astroport-governance/src/emissions_controller/hub.rs +++ b/packages/astroport-governance/src/emissions_controller/hub.rs @@ -246,7 +246,7 @@ pub struct OutpostInfo { pub astro_denom: String, /// A pool that must receive flat ASTRO emissions. Optional. pub astro_pool_config: Option, - /// Defines whether outpost is jailed. Jailed outposts can't participate in the voting process + /// Defines whether outpost is jailed. Jailed outposts can't participate in the voting process, /// but they still allow remote users to unstake their vxASTRO. pub jailed: bool, } diff --git a/schemas/astroport-emissions-controller/astroport-emissions-controller.json b/schemas/astroport-emissions-controller/astroport-emissions-controller.json index 9e144fe..cc98bfe 100644 --- a/schemas/astroport-emissions-controller/astroport-emissions-controller.json +++ b/schemas/astroport-emissions-controller/astroport-emissions-controller.json @@ -1,6 +1,6 @@ { "contract_name": "astroport-emissions-controller", - "contract_version": "1.0.1", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -1121,7 +1121,7 @@ ] }, "jailed": { - "description": "Defines whether outpost is jailed. Jailed outposts can't participate in the voting process but they still allow remote users to unstake their vxASTRO.", + "description": "Defines whether outpost is jailed. Jailed outposts can't participate in the voting process, but they still allow remote users to unstake their vxASTRO.", "type": "boolean" }, "params": { diff --git a/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json b/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json index a1926af..1a38060 100644 --- a/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json +++ b/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json @@ -62,7 +62,7 @@ ] }, "jailed": { - "description": "Defines whether outpost is jailed. Jailed outposts can't participate in the voting process but they still allow remote users to unstake their vxASTRO.", + "description": "Defines whether outpost is jailed. Jailed outposts can't participate in the voting process, but they still allow remote users to unstake their vxASTRO.", "type": "boolean" }, "params": { From 847469033826158714322d33c8c6fe8b62fa4eca Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:26:10 +0400 Subject: [PATCH 4/4] forbid updating jailed outpost --- contracts/emissions_controller/src/execute.rs | 13 ++++++----- .../tests/emissions_controller_integration.rs | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/contracts/emissions_controller/src/execute.rs b/contracts/emissions_controller/src/execute.rs index af3c033..8f2de97 100644 --- a/contracts/emissions_controller/src/execute.rs +++ b/contracts/emissions_controller/src/execute.rs @@ -304,16 +304,17 @@ pub fn update_outpost( ); } - OUTPOSTS.save( - deps.storage, - &prefix, - &OutpostInfo { + OUTPOSTS.update(deps.storage, &prefix, |outpost| match outpost { + Some(OutpostInfo { jailed: true, .. }) => Err(ContractError::JailedOutpost { + prefix: prefix.clone(), + }), + _ => Ok(OutpostInfo { params: outpost_params, astro_denom, astro_pool_config, jailed: false, - }, - )?; + }), + })?; Ok(Response::default().add_attributes([("action", "update_outpost"), ("prefix", &prefix)])) } diff --git a/contracts/emissions_controller/tests/emissions_controller_integration.rs b/contracts/emissions_controller/tests/emissions_controller_integration.rs index fba73f7..57085e7 100644 --- a/contracts/emissions_controller/tests/emissions_controller_integration.rs +++ b/contracts/emissions_controller/tests/emissions_controller_integration.rs @@ -515,6 +515,28 @@ fn test_outpost_management() { ] ); + // Cant update jailed outpost + let err = helper + .app + .execute_contract( + helper.owner.clone(), + helper.emission_controller.clone(), + &ExecuteMsg::Custom(HubMsg::UpdateOutpost { + prefix: "neutron".to_string(), + astro_denom: neutron.astro_denom.clone(), + outpost_params: None, + astro_pool_config: None, + }), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::JailedOutpost { + prefix: "neutron".to_string() + } + ); + // Unjail Neutron let err = helper .app