diff --git a/Cargo.lock b/Cargo.lock index 104a288..41f619b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "astroport-emissions-controller-outpost" -version = "1.0.0" +version = "1.1.0" dependencies = [ "anyhow", "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", @@ -241,6 +241,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw20 1.1.2", "itertools 0.12.1", + "sha2 0.10.8", "thiserror", ] @@ -1034,6 +1035,15 @@ dependencies = [ "cosmwasm-std", ] +[[package]] +name = "ibc-escrow-tester" +version = "0.1.0" +dependencies = [ + "astroport-governance 4.2.0", + "cosmwasm-schema", + "cosmwasm-std", +] + [[package]] name = "integer-sqrt" version = "0.1.5" diff --git a/contracts/emissions_controller/Cargo.toml b/contracts/emissions_controller/Cargo.toml index 2cad512..61a6fc7 100644 --- a/contracts/emissions_controller/Cargo.toml +++ b/contracts/emissions_controller/Cargo.toml @@ -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.0.0" } +astroport-governance = { path = "../../packages/astroport-governance", version = "4.2" } astroport.workspace = true neutron-sdk = "0.10.0" serde_json = "1" diff --git a/contracts/emissions_controller/src/execute.rs b/contracts/emissions_controller/src/execute.rs index 8f2de97..9b3f267 100644 --- a/contracts/emissions_controller/src/execute.rs +++ b/contracts/emissions_controller/src/execute.rs @@ -17,12 +17,14 @@ use neutron_sdk::bindings::query::NeutronQuery; use astroport_governance::emissions_controller::consts::{EPOCH_LENGTH, IBC_TIMEOUT}; use astroport_governance::emissions_controller::hub::{ - AstroPoolConfig, HubMsg, OutpostInfo, OutpostParams, OutpostStatus, TuneInfo, UserInfo, - VotedPoolInfo, + AstroPoolConfig, HubMsg, InputOutpostParams, OutpostInfo, OutpostParams, OutpostStatus, + TuneInfo, UserInfo, VotedPoolInfo, }; use astroport_governance::emissions_controller::msg::{ExecuteMsg, VxAstroIbcMsg}; use astroport_governance::emissions_controller::utils::{check_lp_token, get_voting_power}; -use astroport_governance::utils::check_contract_supports_channel; +use astroport_governance::utils::{ + check_contract_supports_channel, determine_ics20_escrow_address, +}; use astroport_governance::{assembly, voting_escrow}; use crate::error::ContractError; @@ -31,9 +33,8 @@ use crate::state::{ USER_INFO, VOTED_POOLS, }; use crate::utils::{ - build_emission_ibc_msg, determine_outpost_prefix, get_epoch_start, get_outpost_prefix, - min_ntrn_ibc_fee, raw_emissions_to_schedules, simulate_tune, validate_outpost_prefix, - TuneResult, + build_emission_ibc_msg, get_epoch_start, get_outpost_prefix, jail_outpost, min_ntrn_ibc_fee, + raw_emissions_to_schedules, simulate_tune, validate_outpost_prefix, TuneResult, }; /// Exposes all the execute functions available in the contract. @@ -149,7 +150,7 @@ pub fn execute( outpost_params, astro_pool_config, ), - HubMsg::JailOutpost { prefix } => jail_outpost(deps, env, info, prefix), + HubMsg::JailOutpost { prefix } => jail_outpost_endpoint(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), @@ -251,7 +252,7 @@ pub fn update_outpost( info: MessageInfo, prefix: String, astro_denom: String, - outpost_params: Option, + outpost_params: Option, astro_pool_config: Option, ) -> Result, ContractError> { nonpayable(&info)?; @@ -308,12 +309,29 @@ pub fn update_outpost( Some(OutpostInfo { jailed: true, .. }) => Err(ContractError::JailedOutpost { prefix: prefix.clone(), }), - _ => Ok(OutpostInfo { - params: outpost_params, - astro_denom, - astro_pool_config, - jailed: false, - }), + _ => { + let params = outpost_params + .map(|params| -> StdResult<_> { + Ok(OutpostParams { + emissions_controller: params.emissions_controller, + voting_channel: params.voting_channel, + escrow_address: determine_ics20_escrow_address( + deps.api, + "transfer", + ¶ms.ics20_channel, + )?, + ics20_channel: params.ics20_channel, + }) + }) + .transpose()?; + + Ok(OutpostInfo { + params, + astro_denom, + astro_pool_config, + jailed: false, + }) + } })?; Ok(Response::default().add_attributes([("action", "update_outpost"), ("prefix", &prefix)])) @@ -321,7 +339,7 @@ pub fn update_outpost( /// Jails outpost as well as removes all whitelisted /// and being voted pools related to this outpost. -pub fn jail_outpost( +pub fn jail_outpost_endpoint( deps: DepsMut, env: Env, info: MessageInfo, @@ -331,34 +349,7 @@ pub fn jail_outpost( let config = CONFIG.load(deps.storage)?; ensure!(info.sender == config.owner, ContractError::Unauthorized {}); - // Remove all votable pools related to this outpost - let voted_pools = VOTED_POOLS - .keys(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - let prefix_some = Some(prefix.clone()); - voted_pools - .iter() - .filter(|pool| determine_outpost_prefix(pool) == prefix_some) - .try_for_each(|pool| VOTED_POOLS.remove(deps.storage, pool, env.block.time.seconds()))?; - - // And clear whitelist - POOLS_WHITELIST.update::<_, StdError>(deps.storage, |mut whitelist| { - whitelist.retain(|pool| determine_outpost_prefix(pool) != prefix_some); - Ok(whitelist) - })?; - - OUTPOSTS.update(deps.storage, &prefix, |outpost| { - if let Some(outpost) = outpost { - Ok(OutpostInfo { - jailed: true, - ..outpost - }) - } else { - Err(ContractError::OutpostNotFound { - prefix: prefix.clone(), - }) - } - })?; + jail_outpost(deps.storage, &prefix, env)?; Ok(Response::default().add_attributes([("action", "jail_outpost"), ("prefix", &prefix)])) } diff --git a/contracts/emissions_controller/src/ibc.rs b/contracts/emissions_controller/src/ibc.rs index 5cbfaf0..67a99fd 100644 --- a/contracts/emissions_controller/src/ibc.rs +++ b/contracts/emissions_controller/src/ibc.rs @@ -1,13 +1,15 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure, from_json, wasm_execute, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, + ensure, from_json, wasm_execute, Deps, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, StdError, StdResult, + Uint128, }; use astroport_governance::assembly; use astroport_governance::emissions_controller::consts::{IBC_APP_VERSION, IBC_ORDERING}; +use astroport_governance::emissions_controller::hub::OutpostInfo; use astroport_governance::emissions_controller::msg::{ ack_fail, ack_ok, IbcAckResult, VxAstroIbcMsg, }; @@ -15,6 +17,7 @@ use astroport_governance::emissions_controller::msg::{ use crate::error::ContractError; use crate::execute::{handle_update_user, handle_vote}; use crate::state::{get_all_outposts, CONFIG}; +use crate::utils::jail_outpost; #[cfg_attr(not(feature = "library"), entry_point)] pub fn ibc_channel_open( @@ -79,6 +82,43 @@ pub fn ibc_packet_receive( }) } +/// Confirm that total voting power reported from remote outpost doesn't exceed the total xASTRO +/// bridged over (held in escrow). +fn is_outpost_valid( + deps: Deps, + outpost: &OutpostInfo, + ibc_msg: &VxAstroIbcMsg, +) -> Result { + let escrow_address = outpost + .params + .as_ref() + .expect("Outpost params must be set") // It must be guaranteed that params are set + .escrow_address + .clone(); + + let xastro_denom = CONFIG.load(deps.storage)?.xastro_denom; + + let escrow_balance = deps + .querier + .query_balance(escrow_address, xastro_denom)? + .amount; + + match ibc_msg { + VxAstroIbcMsg::EmissionsVote { + total_voting_power, .. + } + | VxAstroIbcMsg::UpdateUserVotes { + total_voting_power, .. + } + | VxAstroIbcMsg::GovernanceVote { + total_voting_power, .. + } => Ok(*total_voting_power <= escrow_balance), + VxAstroIbcMsg::RegisterProposal { .. } => { + unreachable!("Hub can't receive RegisterProposal message") + } + } +} + pub fn do_packet_receive( deps: DepsMut, env: Env, @@ -109,9 +149,9 @@ pub fn do_packet_receive( match ibc_msg { VxAstroIbcMsg::UpdateUserVotes { voter, - voting_power, is_unlock: true, - } => handle_update_user(deps.storage, env, voter.as_str(), voting_power).map( + .. + } => handle_update_user(deps.storage, env, voter.as_str(), Uint128::zero()).map( |orig_response| { IbcReceiveResponse::new() .add_attributes(orig_response.attributes) @@ -121,11 +161,22 @@ pub fn do_packet_receive( _ => Err(ContractError::JailedOutpost { prefix }), } } else { + // Check for possible malicious xASTRO minting behavior on the outpost. + // Jail this outpost in case of total vxASTRO exceeds the total xASTRO bridged over. + if !is_outpost_valid(deps.as_ref(), &outpost, &ibc_msg)? { + jail_outpost(deps.storage, &prefix, env)?; + + return Ok(IbcReceiveResponse::default() + .set_ack(ack_ok()) + .add_attributes([("action", "jail_outpost"), ("prefix", &prefix)])); + } + 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) @@ -147,6 +198,7 @@ pub fn do_packet_receive( voting_power, proposal_id, vote, + .. } => { let config = CONFIG.load(deps.storage)?; let cast_vote_msg = wasm_execute( @@ -211,28 +263,30 @@ mod unit_tests { use std::collections::HashMap; use std::marker::PhantomData; - use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; + use cosmwasm_std::testing::{mock_dependencies, mock_env, MockQuerier, MockStorage}; use cosmwasm_std::{ - to_json_binary, Addr, Decimal, IbcChannel, IbcEndpoint, IbcOrder, IbcPacket, IbcTimeout, - OwnedDeps, Timestamp, + attr, coins, to_json_binary, Addr, Decimal, IbcChannel, IbcEndpoint, IbcOrder, IbcPacket, + IbcTimeout, OwnedDeps, Timestamp, }; + use cw_multi_test::MockApiBech32; use neutron_sdk::bindings::query::NeutronQuery; use astroport_governance::assembly::ProposalVoteOption; use astroport_governance::emissions_controller::hub::{ - OutpostInfo, OutpostParams, VotedPoolInfo, + Config, OutpostInfo, OutpostParams, VotedPoolInfo, }; use astroport_governance::emissions_controller::msg::IbcAckResult; + use astroport_governance::utils::determine_ics20_escrow_address; use crate::state::{OUTPOSTS, POOLS_WHITELIST, VOTED_POOLS}; use super::*; - pub fn mock_custom_dependencies() -> OwnedDeps - { + pub fn mock_custom_dependencies( + ) -> OwnedDeps { OwnedDeps { storage: MockStorage::default(), - api: MockApi::default(), + api: MockApiBech32::new("neutron"), querier: MockQuerier::default(), custom_query_type: PhantomData, } @@ -355,9 +409,34 @@ mod unit_tests { fn test_packet_receive() { let mut deps = mock_custom_dependencies(); + const XASTRO_DENOM: &str = "xastro"; + + CONFIG + .save( + deps.as_mut().storage, + &Config { + owner: Addr::unchecked("".to_string()), + assembly: Addr::unchecked("".to_string()), + vxastro: Addr::unchecked("".to_string()), + factory: Addr::unchecked("".to_string()), + astro_denom: "".to_string(), + xastro_denom: XASTRO_DENOM.to_string(), + staking: Addr::unchecked("".to_string()), + incentives_addr: Addr::unchecked("".to_string()), + pools_per_outpost: 0, + whitelisting_fee: Default::default(), + fee_receiver: Addr::unchecked("".to_string()), + whitelist_threshold: Default::default(), + emissions_multiple: Default::default(), + max_astro: Default::default(), + }, + ) + .unwrap(); + let voting_msg = VxAstroIbcMsg::EmissionsVote { voter: "osmo1voter".to_string(), voting_power: 1000u128.into(), + total_voting_power: Default::default(), votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]), }; let packet = IbcPacket::new( @@ -385,6 +464,9 @@ mod unit_tests { ) ); + let escrow_address = + determine_ics20_escrow_address(deps.as_mut().api, "transfer", "channel-2").unwrap(); + // Mock added outpost and whitelist OUTPOSTS .save( @@ -395,6 +477,7 @@ mod unit_tests { emissions_controller: "".to_string(), voting_channel: "channel-2".to_string(), ics20_channel: "".to_string(), + escrow_address: escrow_address.clone(), }), astro_denom: "".to_string(), astro_pool_config: None, @@ -462,6 +545,7 @@ mod unit_tests { let update_msg = VxAstroIbcMsg::UpdateUserVotes { voter: "osmo1voter".to_string(), voting_power: 2000u128.into(), + total_voting_power: Default::default(), is_unlock: false, }; let packet = IbcPacket::new( @@ -481,6 +565,71 @@ mod unit_tests { let resp = ibc_packet_receive(deps.as_mut().into_empty(), env.clone(), ibc_msg).unwrap(); let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap(); assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into())); + + // Test outpost voting power validation. + + // Set escrow balance to 100_000 xASTRO + deps.querier + .update_balance(&escrow_address, coins(100_000, XASTRO_DENOM)); + + // Emulate outpost total voting power at 99_999 xASTRO + let voting_msg = VxAstroIbcMsg::EmissionsVote { + voter: "osmo1voter2".to_string(), + voting_power: 1000u128.into(), + total_voting_power: 99_999u128.into(), + votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]), + }; + let packet = IbcPacket::new( + to_json_binary(&voting_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(), env.clone(), ibc_msg).unwrap(); + let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap(); + assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into())); + + // Emulate outpost total voting power at 150_000 xASTRO + let voting_msg = VxAstroIbcMsg::EmissionsVote { + voter: "osmo1voter3".to_string(), + voting_power: 1000u128.into(), + total_voting_power: 150_000u128.into(), + votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]), + }; + let packet = IbcPacket::new( + to_json_binary(&voting_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(), env.clone(), ibc_msg).unwrap(); + + assert!(resp.messages.is_empty()); + assert!(resp.events.is_empty()); + assert_eq!( + resp.acknowledgement, + to_json_binary(&IbcAckResult::Ok(b"ok".into())).unwrap() + ); + assert_eq!( + resp.attributes, + vec![attr("action", "jail_outpost"), attr("prefix", "osmo"),] + ); } #[test] @@ -497,6 +646,7 @@ mod unit_tests { emissions_controller: "".to_string(), voting_channel: "channel-2".to_string(), ics20_channel: "".to_string(), + escrow_address: Addr::unchecked("".to_string()), }), astro_denom: "".to_string(), astro_pool_config: None, @@ -510,6 +660,7 @@ mod unit_tests { VxAstroIbcMsg::EmissionsVote { voter: "osmo1voter".to_string(), voting_power: 1000u128.into(), + total_voting_power: Default::default(), votes: HashMap::from([("osmo1pool1".to_string(), Decimal::one())]), }, true, @@ -518,6 +669,7 @@ mod unit_tests { VxAstroIbcMsg::GovernanceVote { voter: "osmo1voter".to_string(), voting_power: 1000u128.into(), + total_voting_power: Default::default(), proposal_id: 1, vote: ProposalVoteOption::For, }, @@ -527,6 +679,7 @@ mod unit_tests { VxAstroIbcMsg::UpdateUserVotes { voter: "osmo1voter".to_string(), voting_power: 2000u128.into(), + total_voting_power: Default::default(), is_unlock: false, }, true, @@ -535,6 +688,7 @@ mod unit_tests { VxAstroIbcMsg::UpdateUserVotes { voter: "osmo1voter".to_string(), voting_power: 0u128.into(), + total_voting_power: Default::default(), is_unlock: true, }, false, diff --git a/contracts/emissions_controller/src/migration.rs b/contracts/emissions_controller/src/migration.rs index ecb8a8b..9a79c08 100644 --- a/contracts/emissions_controller/src/migration.rs +++ b/contracts/emissions_controller/src/migration.rs @@ -3,6 +3,7 @@ use astroport_governance::emissions_controller::hub::{ AstroPoolConfig, OutpostInfo, OutpostParams, }; +use astroport_governance::utils::determine_ics20_escrow_address; use cosmwasm_schema::cw_serde; use cosmwasm_std::{entry_point, DepsMut, Empty, Env, Order, Response, StdResult}; use cw2::{get_contract_version, set_contract_version}; @@ -12,14 +13,21 @@ use crate::error::ContractError; use crate::instantiate::{CONTRACT_NAME, CONTRACT_VERSION}; use crate::state::OUTPOSTS; +#[cw_serde] +struct OldOutpostParams { + pub emissions_controller: String, + pub voting_channel: String, + pub ics20_channel: String, +} + #[cw_serde] struct OldOutpostInfo { - pub params: Option, + pub params: Option, pub astro_denom: String, pub astro_pool_config: Option, } -const OLD_OUTPOSTS: Map<&str, OutpostInfo> = Map::new("outposts"); +const OLD_OUTPOSTS: Map<&str, OldOutpostInfo> = Map::new("outposts"); #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { @@ -33,11 +41,27 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result>>()?; for (prefix, old_outpost) in old_outposts { + let params = old_outpost + .params + .map(|params| -> StdResult<_> { + Ok(OutpostParams { + emissions_controller: params.emissions_controller, + voting_channel: params.voting_channel, + escrow_address: determine_ics20_escrow_address( + deps.api, + "transfer", + ¶ms.ics20_channel, + )?, + ics20_channel: params.ics20_channel, + }) + }) + .transpose()?; + OUTPOSTS.save( deps.storage, &prefix, &OutpostInfo { - params: old_outpost.params, + params, astro_denom: old_outpost.astro_denom, astro_pool_config: old_outpost.astro_pool_config, jailed: false, diff --git a/contracts/emissions_controller/src/utils.rs b/contracts/emissions_controller/src/utils.rs index 09462b9..199b9bc 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, QuerierWrapper, StdError, StdResult, Storage, + coin, Coin, CosmosMsg, Decimal, Deps, Env, Order, 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::{get_active_outposts, TUNE_INFO, VOTED_POOLS}; +use crate::state::{get_active_outposts, OUTPOSTS, POOLS_WHITELIST, TUNE_INFO, VOTED_POOLS}; /// Determine outpost prefix from address or tokenfactory denom. pub fn determine_outpost_prefix(value: &str) -> Option { @@ -364,6 +364,45 @@ pub fn simulate_tune( }) } +/// Jails outpost as well as removes all whitelisted +/// and being voted pools related to this outpost. +pub fn jail_outpost( + storage: &mut dyn Storage, + prefix: &str, + env: Env, +) -> Result<(), ContractError> { + // Remove all votable pools related to this outpost + let voted_pools = VOTED_POOLS + .keys(storage, None, None, Order::Ascending) + .collect::>>()?; + let prefix_some = Some(prefix.to_string()); + voted_pools + .iter() + .filter(|pool| determine_outpost_prefix(pool) == prefix_some) + .try_for_each(|pool| VOTED_POOLS.remove(storage, pool, env.block.time.seconds()))?; + + // And clear whitelist + POOLS_WHITELIST.update::<_, StdError>(storage, |mut whitelist| { + whitelist.retain(|pool| determine_outpost_prefix(pool) != prefix_some); + Ok(whitelist) + })?; + + OUTPOSTS.update(storage, prefix, |outpost| { + if let Some(outpost) = outpost { + Ok(OutpostInfo { + jailed: true, + ..outpost + }) + } else { + Err(ContractError::OutpostNotFound { + prefix: prefix.to_string(), + }) + } + })?; + + Ok(()) +} + #[cfg(test)] mod unit_tests { use super::*; diff --git a/contracts/emissions_controller/tests/common/helper.rs b/contracts/emissions_controller/tests/common/helper.rs index 15032e4..47e7bab 100644 --- a/contracts/emissions_controller/tests/common/helper.rs +++ b/contracts/emissions_controller/tests/common/helper.rs @@ -24,8 +24,8 @@ use astroport_governance::assembly::{ }; use astroport_governance::emissions_controller::consts::EPOCHS_START; use astroport_governance::emissions_controller::hub::{ - EmissionsState, HubInstantiateMsg, HubMsg, OutpostInfo, SimulateTuneResponse, TuneInfo, - UserInfoResponse, VotedPoolInfo, + EmissionsState, HubInstantiateMsg, HubMsg, InputOutpostParams, OutpostInfo, + SimulateTuneResponse, TuneInfo, UserInfoResponse, VotedPoolInfo, }; use astroport_governance::emissions_controller::msg::VxAstroIbcMsg; use astroport_governance::voting_escrow::UpdateMarketingInfo; @@ -508,7 +508,11 @@ impl ControllerHelper { &emissions_controller::msg::ExecuteMsg::Custom(HubMsg::UpdateOutpost { prefix: prefix.to_string(), astro_denom: outpost.astro_denom, - outpost_params: outpost.params, + outpost_params: outpost.params.map(|info| InputOutpostParams { + emissions_controller: info.emissions_controller, + voting_channel: info.voting_channel, + ics20_channel: info.ics20_channel, + }), astro_pool_config: outpost.astro_pool_config, }), &[], diff --git a/contracts/emissions_controller/tests/emissions_controller_integration.rs b/contracts/emissions_controller/tests/emissions_controller_integration.rs index 57085e7..f7f672e 100644 --- a/contracts/emissions_controller/tests/emissions_controller_integration.rs +++ b/contracts/emissions_controller/tests/emissions_controller_integration.rs @@ -17,6 +17,7 @@ use astroport_governance::emissions_controller::hub::{ UserInfoResponse, }; use astroport_governance::emissions_controller::msg::{ExecuteMsg, VxAstroIbcMsg}; +use astroport_governance::utils::determine_ics20_escrow_address; use astroport_governance::{assembly, emissions_controller, voting_escrow}; use astroport_voting_escrow::state::UNLOCK_PERIOD; @@ -248,7 +249,7 @@ fn test_outpost_management() { &ExecuteMsg::Custom(HubMsg::UpdateOutpost { prefix: "neutron".to_string(), astro_denom: neutron.astro_denom.clone(), - outpost_params: neutron.params.clone(), + outpost_params: None, astro_pool_config: neutron.astro_pool_config.clone(), }), &[], @@ -301,12 +302,15 @@ fn test_outpost_management() { .constant_emissions = Uint128::one(); helper.add_outpost("neutron", neutron.clone()).unwrap(); + let osmo_escrow_address = + determine_ics20_escrow_address(helper.app.api(), "transfer", "channel-2").unwrap(); let mut osmosis = OutpostInfo { astro_denom: "uastro".to_string(), params: Some(OutpostParams { emissions_controller: "osmo1controller".to_string(), voting_channel: "channel-1".to_string(), ics20_channel: "channel-2".to_string(), + escrow_address: osmo_escrow_address, }), astro_pool_config: None, jailed: false, @@ -856,6 +860,7 @@ fn test_tune_outpost() { emissions_controller: "osmo1emissionscontroller".to_string(), voting_channel: "channel-1".to_string(), ics20_channel: "channel-2".to_string(), + escrow_address: Addr::unchecked(""), }), astro_pool_config: Some(AstroPoolConfig { astro_pool: astro_pool.to_string(), @@ -1370,6 +1375,7 @@ fn test_some_epochs() { emissions_controller: "osmo1controller".to_string(), voting_channel: "channel-1".to_string(), ics20_channel: "channel-2".to_string(), + escrow_address: Addr::unchecked(""), }), astro_pool_config: None, jailed: false, @@ -1538,6 +1544,7 @@ fn test_interchain_governance() { emissions_controller: "osmo1controller".to_string(), voting_channel: "channel-1".to_string(), ics20_channel: "channel-2".to_string(), + escrow_address: Addr::unchecked(""), }), astro_pool_config: None, jailed: false, @@ -1572,6 +1579,7 @@ fn test_interchain_governance() { .mock_packet_receive(VxAstroIbcMsg::GovernanceVote { voter: "osmo1voter".to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), proposal_id: 1, vote: ProposalVoteOption::For, }) @@ -1589,6 +1597,7 @@ fn test_interchain_governance() { .mock_packet_receive(VxAstroIbcMsg::GovernanceVote { voter: "osmo1voter".to_string(), voting_power: 1_000000u128.into(), + total_voting_power: Default::default(), proposal_id: 3, vote: ProposalVoteOption::For, }) diff --git a/contracts/emissions_controller_outpost/Cargo.toml b/contracts/emissions_controller_outpost/Cargo.toml index 4b3b12c..13109a7 100644 --- a/contracts/emissions_controller_outpost/Cargo.toml +++ b/contracts/emissions_controller_outpost/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-emissions-controller-outpost" -version = "1.0.0" +version = "1.1.0" authors = ["Astroport"] edition = "2021" description = "Astroport vxASTRO Emissions Voting Contract. Outpost version" @@ -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.0.0" } +astroport-governance = { path = "../../packages/astroport-governance", version = "4.2" } astroport.workspace = true serde_json = "1" diff --git a/contracts/emissions_controller_outpost/src/error.rs b/contracts/emissions_controller_outpost/src/error.rs index 8e38983..06be9ca 100644 --- a/contracts/emissions_controller_outpost/src/error.rs +++ b/contracts/emissions_controller_outpost/src/error.rs @@ -48,4 +48,7 @@ pub enum ContractError { #[error("User already voted")] AlreadyVoted {}, + + #[error("Failed to migrate contract")] + MigrationError {}, } diff --git a/contracts/emissions_controller_outpost/src/execute.rs b/contracts/emissions_controller_outpost/src/execute.rs index ba800b4..9f48e98 100644 --- a/contracts/emissions_controller_outpost/src/execute.rs +++ b/contracts/emissions_controller_outpost/src/execute.rs @@ -18,7 +18,9 @@ use astroport_governance::emissions_controller::consts::{IBC_TIMEOUT, MAX_POOLS_ use astroport_governance::emissions_controller::msg::ExecuteMsg; use astroport_governance::emissions_controller::msg::VxAstroIbcMsg; use astroport_governance::emissions_controller::outpost::{Config, OutpostMsg}; -use astroport_governance::emissions_controller::utils::{check_lp_token, get_voting_power}; +use astroport_governance::emissions_controller::utils::{ + check_lp_token, get_total_voting_power, get_voting_power, +}; use astroport_governance::utils::check_contract_supports_channel; use crate::error::ContractError; @@ -271,6 +273,8 @@ pub fn handle_vote( let voting_power = get_voting_power(deps.querier, &config.vxastro, &info.sender, None)?; ensure!(!voting_power.is_zero(), ContractError::ZeroVotingPower {}); + let total_voting_power = get_total_voting_power(deps.querier, &config.vxastro, None)?; + let vote_ibc_msg = prepare_ibc_packet( deps.storage, &env, @@ -278,6 +282,7 @@ pub fn handle_vote( VxAstroIbcMsg::EmissionsVote { voter: info.sender.to_string(), voting_power, + total_voting_power, votes: votes_map, }, config.voting_ibc_channel, @@ -304,6 +309,8 @@ pub fn handle_update_user( attr("new_voting_power", voting_power), ]; + let total_voting_power = get_total_voting_power(deps.querier, &config.vxastro, None)?; + let ibc_msg = prepare_ibc_packet( deps.storage, &env, @@ -311,6 +318,7 @@ pub fn handle_update_user( VxAstroIbcMsg::UpdateUserVotes { voter: voter.to_string(), voting_power, + total_voting_power, is_unlock, }, config.voting_ibc_channel, @@ -387,6 +395,9 @@ pub fn governance_vote( get_voting_power(deps.querier, &config.vxastro, &voter, Some(start_time - 1))?; ensure!(!voting_power.is_zero(), ContractError::ZeroVotingPower {}); + let total_voting_power = + get_total_voting_power(deps.querier, &config.vxastro, Some(start_time - 1))?; + let attrs = vec![ attr("action", "governance_vote"), attr("voter", &info.sender), @@ -400,6 +411,7 @@ pub fn governance_vote( VxAstroIbcMsg::GovernanceVote { voter: voter.clone(), voting_power, + total_voting_power, proposal_id, vote, }, diff --git a/contracts/emissions_controller_outpost/src/lib.rs b/contracts/emissions_controller_outpost/src/lib.rs index e4ede68..54d592a 100644 --- a/contracts/emissions_controller_outpost/src/lib.rs +++ b/contracts/emissions_controller_outpost/src/lib.rs @@ -4,5 +4,6 @@ pub mod state; pub mod error; pub mod ibc; pub mod instantiate; +mod migration; pub mod query; pub mod utils; diff --git a/contracts/emissions_controller_outpost/src/migration.rs b/contracts/emissions_controller_outpost/src/migration.rs new file mode 100644 index 0000000..9e1bef6 --- /dev/null +++ b/contracts/emissions_controller_outpost/src/migration.rs @@ -0,0 +1,29 @@ +#![cfg(not(tarpaulin_include))] + +use cosmwasm_std::{DepsMut, Empty, Env, Response}; +use cw2::{get_contract_version, set_contract_version}; + +use crate::error::ContractError; +use crate::instantiate::{CONTRACT_NAME, CONTRACT_VERSION}; + +#[allow(dead_code)] +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { + let contract_version = get_contract_version(deps.storage)?; + + match contract_version.contract.as_ref() { + CONTRACT_NAME => match contract_version.version.as_ref() { + "1.0.0" => Ok(()), + _ => Err(ContractError::MigrationError {}), + }, + _ => Err(ContractError::MigrationError {}), + }?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("previous_contract_name", &contract_version.contract) + .add_attribute("previous_contract_version", &contract_version.version) + .add_attribute("new_contract_name", CONTRACT_NAME) + .add_attribute("new_contract_version", CONTRACT_VERSION)) +} diff --git a/contracts/emissions_controller_outpost/tests/emissions_controller_outpost_integration.rs b/contracts/emissions_controller_outpost/tests/emissions_controller_outpost_integration.rs index 3644735..90e1aa2 100644 --- a/contracts/emissions_controller_outpost/tests/emissions_controller_outpost_integration.rs +++ b/contracts/emissions_controller_outpost/tests/emissions_controller_outpost_integration.rs @@ -365,6 +365,7 @@ fn test_voting() { VxAstroIbcMsg::UpdateUserVotes { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), is_unlock: false, }, None, @@ -398,6 +399,7 @@ fn test_voting() { let mock_packet = VxAstroIbcMsg::EmissionsVote { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), votes: Default::default(), }; helper.mock_ibc_timeout(mock_packet.clone()).unwrap(); @@ -429,6 +431,7 @@ fn test_voting() { VxAstroIbcMsg::EmissionsVote { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), votes: Default::default(), }, None, @@ -445,6 +448,7 @@ fn test_voting() { let mock_packet = VxAstroIbcMsg::UpdateUserVotes { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), is_unlock: true, }; helper @@ -524,6 +528,7 @@ fn test_unlock_and_withdraw() { VxAstroIbcMsg::UpdateUserVotes { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), is_unlock: false, }, None, @@ -546,6 +551,7 @@ fn test_unlock_and_withdraw() { VxAstroIbcMsg::UpdateUserVotes { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), is_unlock: true, }, None, @@ -659,6 +665,7 @@ fn test_interchain_governance() { VxAstroIbcMsg::UpdateUserVotes { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), is_unlock: false, }, None, @@ -672,6 +679,7 @@ fn test_interchain_governance() { .mock_ibc_timeout(VxAstroIbcMsg::GovernanceVote { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), proposal_id: 2, vote: ProposalVoteOption::For, }) @@ -685,6 +693,7 @@ fn test_interchain_governance() { VxAstroIbcMsg::GovernanceVote { voter: user.to_string(), voting_power: Default::default(), + total_voting_power: Default::default(), proposal_id: 2, vote: ProposalVoteOption::For, }, diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 3bbbe55..4fe168b 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -23,3 +23,4 @@ cosmwasm-schema.workspace = true thiserror.workspace = true astroport.workspace = true itertools.workspace = true +sha2 = "0.10.8" diff --git a/packages/astroport-governance/src/emissions_controller/hub.rs b/packages/astroport-governance/src/emissions_controller/hub.rs index 21908f5..ba7f262 100644 --- a/packages/astroport-governance/src/emissions_controller/hub.rs +++ b/packages/astroport-governance/src/emissions_controller/hub.rs @@ -75,7 +75,7 @@ pub enum HubMsg { /// 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)) - outpost_params: Option, + outpost_params: Option, /// A pool that must receive flat ASTRO emissions. Optional. astro_pool_config: Option, }, @@ -224,6 +224,18 @@ pub struct OutpostParams { pub voting_channel: String, /// General IBC channel for fungible token transfers pub ics20_channel: String, + /// ICS20 transfer escrow address on Neutron. Calculated automatically based on channel id + pub escrow_address: Addr, +} + +#[cw_serde] +pub struct InputOutpostParams { + /// Emissions controller on a given outpost + pub emissions_controller: String, + /// wasm<>wasm IBC channel for voting + pub voting_channel: String, + /// General IBC channel for fungible token transfers + pub ics20_channel: String, } /// Each outpost may have one pool that receives flat ASTRO emissions. diff --git a/packages/astroport-governance/src/emissions_controller/msg.rs b/packages/astroport-governance/src/emissions_controller/msg.rs index c8bd0f7..54b4ecc 100644 --- a/packages/astroport-governance/src/emissions_controller/msg.rs +++ b/packages/astroport-governance/src/emissions_controller/msg.rs @@ -60,6 +60,8 @@ pub enum VxAstroIbcMsg { voter: String, /// Actual voting power reported from outpost voting_power: Uint128, + /// Current total voting power on this outpost + total_voting_power: Uint128, /// Voting power distribution votes: HashMap, }, @@ -68,6 +70,8 @@ pub enum VxAstroIbcMsg { voter: String, /// Actual voting power reported from outpost voting_power: Uint128, + /// Current total voting power on this outpost + total_voting_power: Uint128, /// Marker defines whether this packet was sent from vxASTRO unlock context is_unlock: bool, }, @@ -78,6 +82,8 @@ pub enum VxAstroIbcMsg { voter: String, /// Actual voting power reported from outpost voting_power: Uint128, + /// Current total voting power on this outpost + total_voting_power: Uint128, /// Proposal id proposal_id: u64, /// Vote option diff --git a/packages/astroport-governance/src/emissions_controller/utils.rs b/packages/astroport-governance/src/emissions_controller/utils.rs index 6f034d3..4898367 100644 --- a/packages/astroport-governance/src/emissions_controller/utils.rs +++ b/packages/astroport-governance/src/emissions_controller/utils.rs @@ -74,6 +74,18 @@ pub fn get_voting_power( ) } +#[inline] +pub fn get_total_voting_power( + querier: QuerierWrapper, + vxastro: &Addr, + timestamp: Option, +) -> StdResult { + querier.query_wasm_smart( + vxastro, + &voting_escrow::QueryMsg::TotalVotingPower { timestamp }, + ) +} + #[inline] pub fn query_incentives_addr(querier: QuerierWrapper, factory: &Addr) -> StdResult { querier diff --git a/packages/astroport-governance/src/utils.rs b/packages/astroport-governance/src/utils.rs index 0728dfb..dca86fe 100644 --- a/packages/astroport-governance/src/utils.rs +++ b/packages/astroport-governance/src/utils.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Addr, ChannelResponse, IbcQuery, QuerierWrapper, StdError, StdResult}; +use cosmwasm_std::{Addr, Api, ChannelResponse, IbcQuery, QuerierWrapper, StdError, StdResult}; +use sha2::Digest; /// Checks that a contract supports a given IBC-channel. /// ## Params @@ -26,3 +27,27 @@ pub fn check_contract_supports_channel( )) }) } + +const ESCROW_ADDRESS_VERSION: &str = "ics20-1"; + +/// Derives an escrow address for ICS20 IBC transfers. +/// Replicated logic from https://github.com/cosmos/ibc-go/blob/2beec482dc4b944be5378639cdc90433707a21bd/modules/apps/transfer/types/keys.go#L48-L62 +/// The escrow address follows the format as outlined in ADR 028: +/// https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-028-public-key-addresses.md +pub fn determine_ics20_escrow_address( + api: &dyn Api, + port_id: &str, + channel_id: &str, +) -> StdResult { + // a slash is used to create domain separation between port and channel identifiers to + // prevent address collisions between escrow addresses created for different channels + let contents = format!("{port_id}/{channel_id}"); + + // ADR 028 AddressHash construction + let mut pre_image = ESCROW_ADDRESS_VERSION.as_bytes().to_vec(); + pre_image.push(0); + pre_image.extend_from_slice(contents.as_bytes()); + let hash = sha2::Sha256::digest(&pre_image); + + api.addr_humanize(&hash[..20].into()) +} diff --git a/schemas/astroport-emissions-controller-outpost/astroport-emissions-controller-outpost.json b/schemas/astroport-emissions-controller-outpost/astroport-emissions-controller-outpost.json index d280b07..858fe9b 100644 --- a/schemas/astroport-emissions-controller-outpost/astroport-emissions-controller-outpost.json +++ b/schemas/astroport-emissions-controller-outpost/astroport-emissions-controller-outpost.json @@ -1,6 +1,6 @@ { "contract_name": "astroport-emissions-controller-outpost", - "contract_version": "1.0.0", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -881,11 +881,20 @@ "emissions_vote": { "type": "object", "required": [ + "total_voting_power", "voter", "votes", "voting_power" ], "properties": { + "total_voting_power": { + "description": "Current total voting power on this outpost", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, "voter": { "type": "string" }, @@ -921,6 +930,7 @@ "type": "object", "required": [ "is_unlock", + "total_voting_power", "voter", "voting_power" ], @@ -929,6 +939,14 @@ "description": "Marker defines whether this packet was sent from vxASTRO unlock context", "type": "boolean" }, + "total_voting_power": { + "description": "Current total voting power on this outpost", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, "voter": { "type": "string" }, @@ -987,6 +1005,7 @@ "type": "object", "required": [ "proposal_id", + "total_voting_power", "vote", "voter", "voting_power" @@ -998,6 +1017,14 @@ "format": "uint64", "minimum": 0.0 }, + "total_voting_power": { + "description": "Current total voting power on this outpost", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, "vote": { "description": "Vote option", "allOf": [ diff --git a/schemas/astroport-emissions-controller-outpost/raw/response_to_query_user_ibc_status.json b/schemas/astroport-emissions-controller-outpost/raw/response_to_query_user_ibc_status.json index 0fe5be3..780a9d2 100644 --- a/schemas/astroport-emissions-controller-outpost/raw/response_to_query_user_ibc_status.json +++ b/schemas/astroport-emissions-controller-outpost/raw/response_to_query_user_ibc_status.json @@ -73,11 +73,20 @@ "emissions_vote": { "type": "object", "required": [ + "total_voting_power", "voter", "votes", "voting_power" ], "properties": { + "total_voting_power": { + "description": "Current total voting power on this outpost", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, "voter": { "type": "string" }, @@ -113,6 +122,7 @@ "type": "object", "required": [ "is_unlock", + "total_voting_power", "voter", "voting_power" ], @@ -121,6 +131,14 @@ "description": "Marker defines whether this packet was sent from vxASTRO unlock context", "type": "boolean" }, + "total_voting_power": { + "description": "Current total voting power on this outpost", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, "voter": { "type": "string" }, @@ -179,6 +197,7 @@ "type": "object", "required": [ "proposal_id", + "total_voting_power", "vote", "voter", "voting_power" @@ -190,6 +209,14 @@ "format": "uint64", "minimum": 0.0 }, + "total_voting_power": { + "description": "Current total voting power on this outpost", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, "vote": { "description": "Vote option", "allOf": [ diff --git a/schemas/astroport-emissions-controller/astroport-emissions-controller.json b/schemas/astroport-emissions-controller/astroport-emissions-controller.json index cc98bfe..76e65ab 100644 --- a/schemas/astroport-emissions-controller/astroport-emissions-controller.json +++ b/schemas/astroport-emissions-controller/astroport-emissions-controller.json @@ -589,7 +589,7 @@ "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": [ { - "$ref": "#/definitions/OutpostParams" + "$ref": "#/definitions/InputOutpostParams" }, { "type": "null" @@ -676,7 +676,7 @@ } ] }, - "OutpostParams": { + "InputOutpostParams": { "type": "object", "required": [ "emissions_controller", @@ -1075,6 +1075,10 @@ "minItems": 2 }, "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, "AstroPoolConfig": { "description": "Each outpost may have one pool that receives flat ASTRO emissions. This pools doesn't participate in the voting process.", "type": "object", @@ -1142,6 +1146,7 @@ "type": "object", "required": [ "emissions_controller", + "escrow_address", "ics20_channel", "voting_channel" ], @@ -1150,6 +1155,14 @@ "description": "Emissions controller on a given outpost", "type": "string" }, + "escrow_address": { + "description": "ICS20 transfer escrow address on Neutron. Calculated automatically based on channel id", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "ics20_channel": { "description": "General IBC channel for fungible token transfers", "type": "string" diff --git a/schemas/astroport-emissions-controller/raw/execute.json b/schemas/astroport-emissions-controller/raw/execute.json index 773a591..49d596e 100644 --- a/schemas/astroport-emissions-controller/raw/execute.json +++ b/schemas/astroport-emissions-controller/raw/execute.json @@ -337,7 +337,7 @@ "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": [ { - "$ref": "#/definitions/OutpostParams" + "$ref": "#/definitions/InputOutpostParams" }, { "type": "null" @@ -424,7 +424,7 @@ } ] }, - "OutpostParams": { + "InputOutpostParams": { "type": "object", "required": [ "emissions_controller", 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 1a38060..3879d04 100644 --- a/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json +++ b/schemas/astroport-emissions-controller/raw/response_to_list_outposts.json @@ -16,6 +16,10 @@ "minItems": 2 }, "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, "AstroPoolConfig": { "description": "Each outpost may have one pool that receives flat ASTRO emissions. This pools doesn't participate in the voting process.", "type": "object", @@ -83,6 +87,7 @@ "type": "object", "required": [ "emissions_controller", + "escrow_address", "ics20_channel", "voting_channel" ], @@ -91,6 +96,14 @@ "description": "Emissions controller on a given outpost", "type": "string" }, + "escrow_address": { + "description": "ICS20 transfer escrow address on Neutron. Calculated automatically based on channel id", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "ics20_channel": { "description": "General IBC channel for fungible token transfers", "type": "string"