diff --git a/Cargo.lock b/Cargo.lock index ec03f2f..3e5f986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ version = "3.0.0" dependencies = [ "anyhow", "astroport 4.0.3", - "astroport-governance 4.2.1", + "astroport-governance 4.3.0", "astroport-staking", "astroport-tokenfactory-tracker", "astroport-voting-escrow", @@ -136,13 +136,13 @@ dependencies = [ [[package]] name = "astroport-emissions-controller" -version = "1.1.2" +version = "1.2.0" dependencies = [ "anyhow", "astro-assembly", "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "astroport-factory", - "astroport-governance 4.2.1", + "astroport-governance 4.3.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.2.1", + "astroport-governance 4.3.0", "astroport-incentives", "astroport-pair", "astroport-voting-escrow", @@ -233,7 +233,7 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "4.2.1" +version = "4.3.0" dependencies = [ "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "cosmwasm-schema", @@ -321,7 +321,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.2.1", + "astroport-governance 4.3.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 2aeb38a..caf6a61 100644 --- a/contracts/emissions_controller/Cargo.toml +++ b/contracts/emissions_controller/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-emissions-controller" -version = "1.1.2" +version = "1.2.0" authors = ["Astroport"] edition = "2021" description = "Astroport vxASTRO Emissions Voting Contract" @@ -23,7 +23,7 @@ cw-storage-plus.workspace = true cosmwasm-schema.workspace = true thiserror.workspace = true itertools.workspace = true -astroport-governance = { path = "../../packages/astroport-governance", version = "4.2" } +astroport-governance = { path = "../../packages/astroport-governance", version = "4.3" } astroport.workspace = true neutron-sdk = "0.10.0" serde_json = "1" diff --git a/contracts/emissions_controller/src/error.rs b/contracts/emissions_controller/src/error.rs index d38d808..ce41feb 100644 --- a/contracts/emissions_controller/src/error.rs +++ b/contracts/emissions_controller/src/error.rs @@ -83,4 +83,7 @@ pub enum ContractError { #[error("Outpost {prefix} is jailed. Only vxASTRO unlocks are available")] JailedOutpost { prefix: String }, + + #[error("Pool {0} is blacklisted")] + PoolIsBlacklisted(String), } diff --git a/contracts/emissions_controller/src/execute.rs b/contracts/emissions_controller/src/execute.rs index 9b3f267..761e680 100644 --- a/contracts/emissions_controller/src/execute.rs +++ b/contracts/emissions_controller/src/execute.rs @@ -6,9 +6,9 @@ use astroport::incentives; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, ensure, to_json_binary, wasm_execute, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, - Fraction, IbcMsg, IbcTimeout, MessageInfo, Order, Response, StdError, StdResult, Storage, - Uint128, + attr, ensure, ensure_eq, to_json_binary, wasm_execute, BankMsg, Coin, CosmosMsg, Decimal, + DepsMut, Env, Fraction, IbcMsg, IbcTimeout, MessageInfo, Order, Response, StdError, StdResult, + Storage, Uint128, }; use cw_utils::{must_pay, nonpayable}; use itertools::Itertools; @@ -29,8 +29,8 @@ use astroport_governance::{assembly, voting_escrow}; use crate::error::ContractError; use crate::state::{ - get_active_outposts, CONFIG, OUTPOSTS, OWNERSHIP_PROPOSAL, POOLS_WHITELIST, TUNE_INFO, - USER_INFO, VOTED_POOLS, + get_active_outposts, CONFIG, OUTPOSTS, OWNERSHIP_PROPOSAL, POOLS_BLACKLIST, POOLS_WHITELIST, + TUNE_INFO, USER_INFO, VOTED_POOLS, }; use crate::utils::{ build_emission_ibc_msg, get_epoch_start, get_outpost_prefix, jail_outpost, min_ntrn_ibc_fee, @@ -136,6 +136,9 @@ pub fn execute( } ExecuteMsg::Custom(hub_msg) => match hub_msg { HubMsg::WhitelistPool { lp_token: pool } => whitelist_pool(deps, env, info, pool), + HubMsg::UpdateBlacklist { add, remove } => { + update_blacklist(deps, info, env, add, remove) + } HubMsg::UpdateOutpost { prefix, astro_denom, @@ -191,6 +194,12 @@ pub fn whitelist_pool( ContractError::IncorrectWhitelistFee(config.whitelisting_fee) ); + // Ensure that LP token is not blacklisted + ensure!( + !POOLS_BLACKLIST.has(deps.storage, &pool), + ContractError::PoolIsBlacklisted(pool.clone()) + ); + // Perform basic LP token validation. Ensure the outpost exists. let outposts = get_active_outposts(deps.storage)?; if let Some(prefix) = get_outpost_prefix(&pool, &outposts) { @@ -244,6 +253,63 @@ pub fn whitelist_pool( .add_attributes([attr("action", "whitelist_pool"), attr("pool", &pool)])) } +pub fn update_blacklist( + deps: DepsMut, + info: MessageInfo, + env: Env, + add: Vec, + remove: Vec, +) -> Result, ContractError> { + let config = CONFIG.load(deps.storage)?; + + ensure_eq!(info.sender, config.owner, ContractError::Unauthorized {}); + + // Checking for duplicates + ensure!( + remove.iter().chain(add.iter()).all_unique(), + StdError::generic_err("Duplicated LP tokens found") + ); + + // Remove pools from blacklist + for lp_token in &remove { + ensure!( + POOLS_BLACKLIST.has(deps.storage, lp_token), + StdError::generic_err(format!("LP token {lp_token} wasn't found in the blacklist")) + ); + + POOLS_BLACKLIST.remove(deps.storage, lp_token); + } + + // Add pools to blacklist + for lp_token in &add { + ensure!( + !POOLS_BLACKLIST.has(deps.storage, lp_token), + StdError::generic_err(format!("LP token {lp_token} is already blacklisted")) + ); + + // If key doesn't exist .remove() doesn't throw an error + VOTED_POOLS.remove(deps.storage, lp_token, env.block.time.seconds())?; + POOLS_BLACKLIST.save(deps.storage, lp_token, &())?; + } + + // And remove pools from the whitelist if they are there + POOLS_WHITELIST.update::<_, StdError>(deps.storage, |mut whitelist| { + whitelist.retain(|pool| !add.contains(pool)); + Ok(whitelist) + })?; + + let mut attrs = vec![attr("action", "update_blacklist")]; + + if !add.is_empty() { + attrs.push(attr("add", add.into_iter().join(","))); + } + if !remove.is_empty() { + attrs.push(attr("remove", remove.into_iter().join(","))); + } + + Ok(Response::default().add_attributes(attrs)) +} + /// Permissioned endpoint to add or update outpost. /// Performs several simple checks to cut off possible human errors. pub fn update_outpost( diff --git a/contracts/emissions_controller/src/query.rs b/contracts/emissions_controller/src/query.rs index f2f5eec..f50b2a0 100644 --- a/contracts/emissions_controller/src/query.rs +++ b/contracts/emissions_controller/src/query.rs @@ -14,8 +14,8 @@ use astroport_governance::emissions_controller::hub::{ use crate::error::ContractError; use crate::state::{ - get_active_outposts, get_all_outposts, CONFIG, POOLS_WHITELIST, TUNE_INFO, USER_INFO, - VOTED_POOLS, + get_active_outposts, get_all_outposts, CONFIG, POOLS_BLACKLIST, POOLS_WHITELIST, TUNE_INFO, + USER_INFO, VOTED_POOLS, }; use crate::utils::simulate_tune; @@ -122,6 +122,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + let limit = limit.unwrap_or(MAX_PAGE_LIMIT) as usize; + let start_after = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); + let pools_blacklist = POOLS_BLACKLIST + .keys(deps.storage, start_after, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + Ok(to_json_binary(&pools_blacklist)?) + } QueryMsg::CheckWhitelist { lp_tokens } => { let whitelist = POOLS_WHITELIST.load(deps.storage)?; let is_whitelisted = lp_tokens diff --git a/contracts/emissions_controller/src/state.rs b/contracts/emissions_controller/src/state.rs index 28b2a5a..a99c565 100644 --- a/contracts/emissions_controller/src/state.rs +++ b/contracts/emissions_controller/src/state.rs @@ -13,6 +13,7 @@ pub const CONFIG: Item = Item::new("config"); pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); /// Array of pools eligible for voting. pub const POOLS_WHITELIST: Item> = Item::new("pools_whitelist"); +pub const POOLS_BLACKLIST: Map<&str, ()> = Map::new("pools_blacklist"); /// Registered Astroport outposts with respective parameters. pub const OUTPOSTS: Map<&str, OutpostInfo> = Map::new("outposts"); /// Historical user's voting information. diff --git a/contracts/emissions_controller/tests/common/helper.rs b/contracts/emissions_controller/tests/common/helper.rs index 81674be..493c92d 100644 --- a/contracts/emissions_controller/tests/common/helper.rs +++ b/contracts/emissions_controller/tests/common/helper.rs @@ -501,6 +501,20 @@ impl ControllerHelper { ) } + pub fn update_blacklist( + &mut self, + user: &Addr, + add: Vec, + remove: Vec, + ) -> AnyResult { + self.app.execute_contract( + user.clone(), + self.emission_controller.clone(), + &emissions_controller::msg::ExecuteMsg::Custom(HubMsg::UpdateBlacklist { add, remove }), + &[], + ) + } + pub fn add_outpost(&mut self, prefix: &str, outpost: OutpostInfo) -> AnyResult { self.app.execute_contract( self.owner.clone(), @@ -573,6 +587,16 @@ impl ControllerHelper { ) } + pub fn query_blacklist(&self) -> StdResult> { + self.app.wrap().query_wasm_smart( + &self.emission_controller, + &emissions_controller::hub::QueryMsg::QueryBlacklist { + limit: Some(100), + start_after: None, + }, + ) + } + pub fn check_whitelist(&self, lp_tokens: Vec) -> StdResult> { self.app.wrap().query_wasm_smart( &self.emission_controller, diff --git a/contracts/emissions_controller/tests/emissions_controller_integration.rs b/contracts/emissions_controller/tests/emissions_controller_integration.rs index 279abe0..cf8b3a0 100644 --- a/contracts/emissions_controller/tests/emissions_controller_integration.rs +++ b/contracts/emissions_controller/tests/emissions_controller_integration.rs @@ -14,7 +14,7 @@ use astroport_governance::assembly::{ProposalVoteOption, ProposalVoterResponse}; use astroport_governance::emissions_controller::consts::{DAY, EPOCH_LENGTH}; use astroport_governance::emissions_controller::hub::{ AstroPoolConfig, EmissionsState, HubMsg, OutpostInfo, OutpostParams, OutpostStatus, TuneInfo, - UserInfoResponse, + UserInfoResponse, VotedPoolInfo, }; use astroport_governance::emissions_controller::msg::{ExecuteMsg, VxAstroIbcMsg}; use astroport_governance::utils::determine_ics20_escrow_address; @@ -133,7 +133,7 @@ pub fn voting_test() { } #[test] -fn test_whitelist() { +fn test_whitelist_blacklist() { let mut helper = ControllerHelper::new(); let owner = helper.owner.clone(); let whitelist_fee = helper.whitelisting_fee.clone(); @@ -204,6 +204,12 @@ fn test_whitelist() { .whitelist(&owner, &lp_token, &[whitelist_fee.clone()]) .unwrap(); + // Vote for this pool + helper.lock(&owner, 1000).unwrap(); + helper + .vote(&owner, &[(lp_token.to_string(), Decimal::one())]) + .unwrap(); + let fee_receiver = helper.query_config().unwrap().fee_receiver; let fee_balance = helper .app @@ -223,27 +229,146 @@ fn test_whitelist() { ContractError::PoolAlreadyWhitelisted(lp_token.to_string()) ); - let whitelist = helper.query_whitelist().unwrap(); - assert_eq!(whitelist, vec![lp_token.to_string()]); + let lp_token2 = helper.create_pair("token1", "token3"); + helper + .mint_tokens(&owner, &[whitelist_fee.clone()]) + .unwrap(); + helper + .whitelist(&owner, &lp_token2, &[whitelist_fee.clone()]) + .unwrap(); + + let whitelist = helper + .query_whitelist() + .unwrap() + .into_iter() + .sorted() + .collect_vec(); + assert_eq!(whitelist, vec![lp_token.clone(), lp_token2.clone()]); let check_result = helper .check_whitelist(vec![ - lp_token.to_string(), - "random_lp".to_string(), + lp_token.clone(), + lp_token2.clone(), "factory/neutron1invalidaddr/astroport/share".to_string(), ]) .unwrap(); assert_eq!( check_result, vec![ - (lp_token.to_string(), true), - ("random_lp".to_string(), false), + (lp_token.clone(), true), + (lp_token2.clone(), true), ( "factory/neutron1invalidaddr/astroport/share".to_string(), false ) ] ); + + let random_user = helper.app.api().addr_make("random"); + + let err = helper + .update_blacklist(&random_user, vec![lp_token.clone()], vec![]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + let err = helper + .update_blacklist(&owner, vec![], vec![lp_token.clone()]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + format!("Generic error: LP token {lp_token} wasn't found in the blacklist") + ); + + let err = helper + .update_blacklist(&owner, vec![lp_token.clone()], vec![lp_token.clone()]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Duplicated LP tokens found" + ); + + // Confirm that pool has voting info before blacklisting + let vote_info = helper.query_voted_pool(&lp_token, None).unwrap(); + assert_eq!( + vote_info, + VotedPoolInfo { + init_ts: 1716768000, + voting_power: 1000u128.into() + } + ); + + helper + .update_blacklist( + &owner, + vec![lp_token.clone(), "neutron1future_lp_token".to_string()], + vec![], + ) + .unwrap(); + + // Try to query voting info for blacklisted pool + let err = helper.query_voted_pool(&lp_token, None).unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Querier contract error: Generic error: Voted pool not found at 1716768000" + ); + + // Try to blacklist same pool again + let err = helper + .update_blacklist(&owner, vec!["neutron1future_lp_token".to_string()], vec![]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: LP token neutron1future_lp_token is already blacklisted" + ); + + let blacklist = helper + .query_blacklist() + .unwrap() + .into_iter() + .sorted() + .collect_vec(); + assert_eq!( + blacklist, + vec![lp_token.clone(), "neutron1future_lp_token".to_string()] + ); + + let whitelist = helper.query_whitelist().unwrap(); + assert_eq!(whitelist, vec![lp_token2.clone()]); + + // Try to whitelist blacklisted pool + let err = helper + .whitelist(&owner, &lp_token, &[whitelist_fee.clone()]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::PoolIsBlacklisted(lp_token.to_string()) + ); + + // Add and remove pools from the blacklist in one go + helper + .update_blacklist( + &owner, + vec!["neutron1one_more_blacklisted_pool".to_string()], + vec!["neutron1future_lp_token".to_string()], + ) + .unwrap(); + + let blacklist = helper + .query_blacklist() + .unwrap() + .into_iter() + .sorted() + .collect_vec(); + assert_eq!( + blacklist, + vec![ + lp_token.clone(), + "neutron1one_more_blacklisted_pool".to_string() + ] + ); } #[test] diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 282a846..8b4e3aa 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "4.2.1" +version = "4.3.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 9105c43..fc7a512 100644 --- a/packages/astroport-governance/src/emissions_controller/hub.rs +++ b/packages/astroport-governance/src/emissions_controller/hub.rs @@ -66,6 +66,20 @@ pub enum HubMsg { }, /// Whitelists a pool to receive ASTRO emissions. Requires fee payment WhitelistPool { lp_token: String }, + /// Manages pool blacklist. + /// Blacklisting prevents voting for it. + /// If the pool is whitelisted, it will be removed from the whitelist. + /// All its votes will be forfeited immediately. + /// Users will be able to apply their votes to other pools at the next epoch (if they already voted). + /// Removing a pool from the blacklist will not restore the votes + /// and will not add it to the whitelist automatically. + /// Only contract owner can call this endpoint. + UpdateBlacklist { + #[serde(default)] + add: Vec, + #[serde(default)] + remove: Vec, + }, /// Register or update an outpost UpdateOutpost { /// Bech32 prefix @@ -132,6 +146,14 @@ pub enum QueryMsg { limit: Option, start_after: Option, }, + /// QueryBlacklist returns the list of pools that are not allowed to be voted for. + /// The query is paginated. + /// If 'start_after' is provided, it yields a list **excluding** 'start_after'. + #[returns(Vec)] + QueryBlacklist { + limit: Option, + start_after: Option, + }, /// CheckWhitelist checks all the pools in the list and returns whether they are whitelisted. /// Returns array of tuples (LP token, is_whitelisted). #[returns(Vec<(String, bool)>)] diff --git a/schemas/astroport-emissions-controller/astroport-emissions-controller.json b/schemas/astroport-emissions-controller/astroport-emissions-controller.json index 712bbc2..5335a45 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.1.2", + "contract_version": "1.2.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -556,6 +556,36 @@ }, "additionalProperties": false }, + { + "description": "Manages pool blacklist. Blacklisting prevents voting for it. If the pool is whitelisted, it will be removed from the whitelist. All its votes will be forfeited immediately. Users will be able to apply their votes to other pools at the next epoch (if they already voted). Removing a pool from the blacklist will not restore the votes and will not add it to the whitelist automatically. Only contract owner can call this endpoint.", + "type": "object", + "required": [ + "update_blacklist" + ], + "properties": { + "update_blacklist": { + "type": "object", + "properties": { + "add": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "remove": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Register or update an outpost", "type": "object", @@ -882,6 +912,36 @@ }, "additionalProperties": false }, + { + "description": "QueryBlacklist returns the list of pools that are not allowed to be voted for. The query is paginated. If 'start_after' is provided, it yields a list **excluding** 'start_after'.", + "type": "object", + "required": [ + "query_blacklist" + ], + "properties": { + "query_blacklist": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "CheckWhitelist checks all the pools in the list and returns whether they are whitelisted. Returns array of tuples (LP token, is_whitelisted).", "type": "object", @@ -1223,6 +1283,14 @@ } } }, + "query_blacklist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + }, "query_whitelist": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Array_of_String", diff --git a/schemas/astroport-emissions-controller/raw/execute.json b/schemas/astroport-emissions-controller/raw/execute.json index 49d596e..55b58d5 100644 --- a/schemas/astroport-emissions-controller/raw/execute.json +++ b/schemas/astroport-emissions-controller/raw/execute.json @@ -304,6 +304,36 @@ }, "additionalProperties": false }, + { + "description": "Manages pool blacklist. Blacklisting prevents voting for it. If the pool is whitelisted, it will be removed from the whitelist. All its votes will be forfeited immediately. Users will be able to apply their votes to other pools at the next epoch (if they already voted). Removing a pool from the blacklist will not restore the votes and will not add it to the whitelist automatically. Only contract owner can call this endpoint.", + "type": "object", + "required": [ + "update_blacklist" + ], + "properties": { + "update_blacklist": { + "type": "object", + "properties": { + "add": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "remove": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Register or update an outpost", "type": "object", diff --git a/schemas/astroport-emissions-controller/raw/query.json b/schemas/astroport-emissions-controller/raw/query.json index e5d3b1e..447d4df 100644 --- a/schemas/astroport-emissions-controller/raw/query.json +++ b/schemas/astroport-emissions-controller/raw/query.json @@ -175,6 +175,36 @@ }, "additionalProperties": false }, + { + "description": "QueryBlacklist returns the list of pools that are not allowed to be voted for. The query is paginated. If 'start_after' is provided, it yields a list **excluding** 'start_after'.", + "type": "object", + "required": [ + "query_blacklist" + ], + "properties": { + "query_blacklist": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "CheckWhitelist checks all the pools in the list and returns whether they are whitelisted. Returns array of tuples (LP token, is_whitelisted).", "type": "object", diff --git a/schemas/astroport-emissions-controller/raw/response_to_query_blacklist.json b/schemas/astroport-emissions-controller/raw/response_to_query_blacklist.json new file mode 100644 index 0000000..4290cb1 --- /dev/null +++ b/schemas/astroport-emissions-controller/raw/response_to_query_blacklist.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } +}