diff --git a/Cargo.lock b/Cargo.lock index 3596d7f..2b781f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ version = "3.0.0" dependencies = [ "anyhow", "astroport 4.0.3", - "astroport-governance 4.0.0", + "astroport-governance 4.1.0", "astroport-staking", "astroport-tokenfactory-tracker", "astroport-voting-escrow", @@ -136,13 +136,13 @@ dependencies = [ [[package]] name = "astroport-emissions-controller" -version = "1.0.0" +version = "1.0.1" dependencies = [ "anyhow", "astro-assembly", "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "astroport-factory", - "astroport-governance 4.0.0", + "astroport-governance 4.1.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.0.0", + "astroport-governance 4.1.0", "astroport-incentives", "astroport-pair", "astroport-voting-escrow", @@ -233,7 +233,7 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "4.0.0" +version = "4.1.0" dependencies = [ "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", "cosmwasm-schema", @@ -317,10 +317,10 @@ dependencies = [ [[package]] name = "astroport-voting-escrow" -version = "1.0.0" +version = "1.1.0" dependencies = [ "astroport 5.3.0 (git+https://github.com/astroport-fi/astroport-core)", - "astroport-governance 4.0.0", + "astroport-governance 4.1.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 1.2.0", diff --git a/assets/emissions_controller_general.png b/assets/emissions_controller_general.png index 8d9868c..9de9e40 100644 Binary files a/assets/emissions_controller_general.png and b/assets/emissions_controller_general.png differ diff --git a/contracts/emissions_controller/Cargo.toml b/contracts/emissions_controller/Cargo.toml index 7f4344f..7bd80b3 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.0" +version = "1.0.1" authors = ["Astroport"] edition = "2021" description = "Astroport vxASTRO Emissions Voting Contract" diff --git a/contracts/emissions_controller/README.md b/contracts/emissions_controller/README.md index 14466c5..b921090 100644 --- a/contracts/emissions_controller/README.md +++ b/contracts/emissions_controller/README.md @@ -19,7 +19,8 @@ If the pool is located on the Hub contract also checks, this LP token correspond ## Voting Users are required to have vxASTRO to cast their votes. -They can vote for up to five whitelisted pools at once every 10 days. +They can vote for whitelisted pools at once every epoch. +Vote changes are not allowed after votes are cast. After voting, they can't change their votes until the cooldown period ends. Executable message accepts an array of tuples with LP token and vote weight. Vote weight is a number between 0 and 1. Total vote weight can't exceed 1. diff --git a/contracts/emissions_controller/src/error.rs b/contracts/emissions_controller/src/error.rs index 352a702..4a4993a 100644 --- a/contracts/emissions_controller/src/error.rs +++ b/contracts/emissions_controller/src/error.rs @@ -3,8 +3,6 @@ use cw_utils::{ParseReplyError, PaymentError}; use neutron_sdk::NeutronError; use thiserror::Error; -use astroport_governance::emissions_controller::consts::MAX_POOLS_TO_VOTE; - /// This enum describes contract errors #[derive(Error, Debug, PartialEq)] pub enum ContractError { @@ -65,9 +63,6 @@ pub enum ContractError { #[error("Failed to determine outpost for pool {0}")] NoOutpostForPool(String), - #[error("You can vote maximum for {MAX_POOLS_TO_VOTE} pools")] - ExceededMaxPoolsToVote {}, - #[error("Message contains duplicated pools")] DuplicatedVotes {}, @@ -79,4 +74,7 @@ pub enum ContractError { #[error("Can't set zero emissions for astro pool")] ZeroAstroEmissions {}, + + #[error("Failed to migrate contract")] + MigrationError {}, } diff --git a/contracts/emissions_controller/src/execute.rs b/contracts/emissions_controller/src/execute.rs index 4232091..1b088d7 100644 --- a/contracts/emissions_controller/src/execute.rs +++ b/contracts/emissions_controller/src/execute.rs @@ -15,9 +15,7 @@ use itertools::Itertools; use neutron_sdk::bindings::msg::NeutronMsg; use neutron_sdk::bindings::query::NeutronQuery; -use astroport_governance::emissions_controller::consts::{ - EPOCH_LENGTH, IBC_TIMEOUT, MAX_POOLS_TO_VOTE, VOTE_COOLDOWN, -}; +use astroport_governance::emissions_controller::consts::{EPOCH_LENGTH, IBC_TIMEOUT}; use astroport_governance::emissions_controller::hub::{ AstroPoolConfig, HubMsg, OutpostInfo, OutpostParams, OutpostStatus, TuneInfo, UserInfo, VotedPoolInfo, @@ -53,10 +51,6 @@ pub fn execute( votes.len() == votes_map.len(), ContractError::DuplicatedVotes {} ); - ensure!( - votes_map.len() <= MAX_POOLS_TO_VOTE, - ContractError::ExceededMaxPoolsToVote {} - ); let deps = deps.into_empty(); let config = CONFIG.load(deps.storage)?; let voting_power = get_voting_power(deps.querier, &config.vxastro, &info.sender, None)?; @@ -415,7 +409,7 @@ pub fn retry_failed_outposts( } /// The function checks that: -/// * user didn't vote for the last 10 days, +/// * user didn't vote at the current epoch, /// * sum of all percentage values <= 1. /// User can direct his voting power partially. /// @@ -434,10 +428,12 @@ pub fn handle_vote( ) -> Result, ContractError> { let user_info = USER_INFO.may_load(deps.storage, voter)?.unwrap_or_default(); let block_ts = env.block.time.seconds(); - // Is the user eligible to vote again? + + let epoch_start = get_epoch_start(block_ts); + // User can vote once per epoch ensure!( - user_info.vote_ts + VOTE_COOLDOWN <= block_ts, - ContractError::VoteCooldown(user_info.vote_ts + VOTE_COOLDOWN) + user_info.vote_ts < epoch_start, + ContractError::VoteCooldown(epoch_start + EPOCH_LENGTH) ); let mut total_weight = Decimal::zero(); diff --git a/contracts/emissions_controller/src/ibc.rs b/contracts/emissions_controller/src/ibc.rs index ee8d51f..36c4fc6 100644 --- a/contracts/emissions_controller/src/ibc.rs +++ b/contracts/emissions_controller/src/ibc.rs @@ -386,7 +386,10 @@ mod unit_tests { POOLS_WHITELIST .save(deps.as_mut().storage, &vec!["osmo1pool1".to_string()]) .unwrap(); - let env = mock_env(); + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(1724922008); + VOTED_POOLS .save( deps.as_mut().storage, @@ -404,12 +407,12 @@ mod unit_tests { let ack_err: IbcAckResult = from_json(resp.acknowledgement).unwrap(); assert_eq!(ack_err, IbcAckResult::Ok(b"ok".into())); - // The same user has voting cooldown for 10 days + // The same user can only vote at the next epoch 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::Error("Next time you can change your vote is at 1572661419".to_string()) + IbcAckResult::Error("Next time you can change your vote is at 1725840000".to_string()) ); // Voting from random channel is not possible diff --git a/contracts/emissions_controller/src/lib.rs b/contracts/emissions_controller/src/lib.rs index 0f1541f..68e147d 100644 --- a/contracts/emissions_controller/src/lib.rs +++ b/contracts/emissions_controller/src/lib.rs @@ -4,6 +4,7 @@ pub mod state; pub mod error; pub mod ibc; pub mod instantiate; +pub mod migration; pub mod query; pub mod sudo; pub mod utils; diff --git a/contracts/emissions_controller/src/migration.rs b/contracts/emissions_controller/src/migration.rs new file mode 100644 index 0000000..299bcfe --- /dev/null +++ b/contracts/emissions_controller/src/migration.rs @@ -0,0 +1,28 @@ +#![cfg(not(tarpaulin_include))] + +use cosmwasm_std::{entry_point, DepsMut, Empty, Env, Response}; +use cw2::{get_contract_version, set_contract_version}; + +use crate::error::ContractError; +use crate::instantiate::{CONTRACT_NAME, CONTRACT_VERSION}; + +#[cfg_attr(not(feature = "library"), 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/tests/common/helper.rs b/contracts/emissions_controller/tests/common/helper.rs index f5cba6f..15032e4 100644 --- a/contracts/emissions_controller/tests/common/helper.rs +++ b/contracts/emissions_controller/tests/common/helper.rs @@ -380,6 +380,30 @@ impl ControllerHelper { ) } + pub fn instant_unlock(&mut self, user: &Addr, amount: u128) -> AnyResult { + self.app.execute_contract( + user.clone(), + self.vxastro.clone(), + &voting_escrow::ExecuteMsg::InstantUnlock { + amount: amount.into(), + }, + &[], + ) + } + + pub fn set_privileged_list( + &mut self, + sender: &Addr, + list: Vec, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.vxastro.clone(), + &voting_escrow::ExecuteMsg::SetPrivilegedList { list }, + &[], + ) + } + pub fn relock(&mut self, user: &Addr) -> AnyResult { self.app.execute_contract( user.clone(), @@ -411,6 +435,13 @@ impl ControllerHelper { ) } + pub fn total_vp(&self, timestamp: Option) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.vxastro, + &voting_escrow::QueryMsg::TotalVotingPower { timestamp }, + ) + } + pub fn user_info(&self, user: &Addr, timestamp: Option) -> 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 cae95bf..93dd445 100644 --- a/contracts/emissions_controller/tests/emissions_controller_integration.rs +++ b/contracts/emissions_controller/tests/emissions_controller_integration.rs @@ -1,24 +1,23 @@ -use astroport::asset::AssetInfo; -use astroport::common::LP_SUBDENOM; -use astroport::incentives::RewardType; -use cosmwasm_std::{coin, coins, Decimal, Decimal256, Empty, Event, Uint128}; +use std::collections::HashMap; +use std::str::FromStr; + +use astroport::{asset::AssetInfo, common::LP_SUBDENOM, incentives::RewardType}; +use cosmwasm_std::{coin, coins, Addr, Decimal, Decimal256, Empty, Event, Uint128}; use cw_multi_test::Executor; use cw_utils::PaymentError; use itertools::Itertools; use neutron_sdk::sudo::msg::{RequestPacket, TransferSudoMsg}; -use std::collections::HashMap; -use std::str::FromStr; use astroport_emissions_controller::error::ContractError; use astroport_emissions_controller::utils::get_epoch_start; use astroport_governance::assembly::{ProposalVoteOption, ProposalVoterResponse}; -use astroport_governance::emissions_controller::consts::{DAY, EPOCH_LENGTH, VOTE_COOLDOWN}; +use astroport_governance::emissions_controller::consts::{DAY, EPOCH_LENGTH}; use astroport_governance::emissions_controller::hub::{ AstroPoolConfig, EmissionsState, HubMsg, OutpostInfo, OutpostParams, OutpostStatus, TuneInfo, UserInfoResponse, }; use astroport_governance::emissions_controller::msg::{ExecuteMsg, VxAstroIbcMsg}; -use astroport_governance::{assembly, emissions_controller}; +use astroport_governance::{assembly, emissions_controller, voting_escrow}; use astroport_voting_escrow::state::UNLOCK_PERIOD; use crate::common::helper::{ControllerHelper, PROPOSAL_VOTING_PERIOD}; @@ -87,24 +86,6 @@ pub fn voting_test() { ContractError::InvalidTotalWeight {} ); - let err = helper - .vote( - &user, - &[ - (lp_token1.to_string(), Decimal::raw(1)), - (lp_token2.to_string(), Decimal::raw(1)), - ("lp_token3".to_string(), Decimal::raw(1)), - ("lp_token4".to_string(), Decimal::raw(1)), - ("lp_token5".to_string(), Decimal::raw(1)), - ("lp_token6".to_string(), Decimal::raw(1)), - ], - ) - .unwrap_err(); - assert_eq!( - err.downcast::().unwrap(), - ContractError::ExceededMaxPoolsToVote {} - ); - helper .vote(&user, &[(lp_token1.to_string(), Decimal::one())]) .unwrap(); @@ -119,12 +100,13 @@ pub fn voting_test() { helper.timetravel(1); let block_time = helper.app.block_info().time.seconds(); + let epoch_start = get_epoch_start(block_time); assert_eq!( err.downcast::().unwrap(), - ContractError::VoteCooldown(block_time + VOTE_COOLDOWN - 1) + ContractError::VoteCooldown(epoch_start + EPOCH_LENGTH) ); - helper.timetravel(VOTE_COOLDOWN + 1); + helper.timetravel(epoch_start + EPOCH_LENGTH); helper .vote( &user, @@ -394,7 +376,7 @@ fn test_outpost_management() { .unwrap(); // Whitelist astro pool on Osmosis before marking it as ASTRO pool with flat emissions - let osmosis_astro_pool = format!("factory/osmo1pool/{LP_SUBDENOM}"); + let osmosis_astro_pool = format!("factory/osmo1pool/{}", LP_SUBDENOM); helper .mint_tokens(&user, &[helper.whitelisting_fee.clone()]) .unwrap(); @@ -1094,6 +1076,125 @@ fn test_lock_unlock_vxastro() { assert_eq!(bob_balance, coin(1_000000, &helper.xastro)); } +#[test] +fn test_instant_unlock_vxastro() { + let mut helper = ControllerHelper::new(); + + let owner = helper.owner.clone(); + helper + .mint_tokens(&owner, &[coin(1000_000000, helper.astro.clone())]) + .unwrap(); + let whitelisting_fee = helper.whitelisting_fee.clone(); + + helper + .add_outpost( + "neutron", + OutpostInfo { + astro_denom: helper.astro.clone(), + params: None, + astro_pool_config: None, + }, + ) + .unwrap(); + + let pool1 = helper.create_pair("token1", "token2"); + helper + .whitelist(&owner, &pool1, &[whitelisting_fee.clone()]) + .unwrap(); + let pool2 = helper.create_pair("token1", "token3"); + helper + .whitelist(&owner, &pool2, &[whitelisting_fee.clone()]) + .unwrap(); + + let alice = helper.app.api().addr_make("alice"); + helper.lock(&alice, 10_000000).unwrap(); + + helper + .vote( + &alice, + &[ + (pool1.to_string(), Decimal::percent(50)), + (pool2.to_string(), Decimal::percent(50)), + ], + ) + .unwrap(); + + // Assert pools voting power + for pool in [&pool1, &pool2] { + let pool_vp = helper.query_pool_vp(pool.as_str(), None).unwrap(); + assert_eq!(pool_vp.u128(), 5_000000); + } + + // Ensure random user can't instantly unlock + let random = helper.app.api().addr_make("random"); + let err = helper.instant_unlock(&random, 100).unwrap_err(); + assert_eq!( + err.downcast::() + .unwrap(), + astroport_voting_escrow::error::ContractError::Unauthorized {} + ); + + let err = helper + .set_privileged_list(&random, vec![alice.to_string()]) + .unwrap_err(); + assert_eq!( + err.downcast::() + .unwrap(), + astroport_voting_escrow::error::ContractError::Unauthorized {} + ); + + // Add Alice to the privileged list + helper + .set_privileged_list(&owner, vec![alice.to_string()]) + .unwrap(); + + // Ensure alice is added + let privileged_list: Vec = helper + .app + .wrap() + .query_wasm_smart(&helper.vxastro, &voting_escrow::QueryMsg::PrivilegedList {}) + .unwrap(); + assert_eq!(privileged_list, vec![alice.clone()]); + + // Alice instantly unlocks + helper.instant_unlock(&alice, 2_000000).unwrap(); + + let xastro_bal = helper + .app + .wrap() + .query_balance(&alice, &helper.xastro) + .unwrap(); + assert_eq!(xastro_bal.amount.u128(), 2_000000); + + // Assert pools voting power is reduced + for pool in [&pool1, &pool2] { + let pool_vp = helper.query_pool_vp(pool.as_str(), None).unwrap(); + assert_eq!(pool_vp.u128(), 4_000000); + } + + // Total voting power must be 8 + let total_vp = helper.total_vp(None).unwrap(); + assert_eq!(total_vp.u128(), 8_000000); + + let lock_info: voting_escrow::LockInfoResponse = helper + .app + .wrap() + .query_wasm_smart( + &helper.vxastro, + &voting_escrow::QueryMsg::LockInfo { + user: alice.to_string(), + }, + ) + .unwrap(); + assert_eq!( + lock_info, + voting_escrow::LockInfoResponse { + amount: 8_000000u128.into(), + unlock_status: None + } + ); +} + #[test] fn test_some_epochs() { let mut helper = ControllerHelper::new(); diff --git a/contracts/emissions_controller_outpost/README.md b/contracts/emissions_controller_outpost/README.md index 9bc6efd..e3dd262 100644 --- a/contracts/emissions_controller_outpost/README.md +++ b/contracts/emissions_controller_outpost/README.md @@ -3,7 +3,8 @@ The Emissions Controller Outpost is a lightweight satellite for the main Emissions Controller located on the Hub. For the vxASTRO staker perspective, this contract has the same API as the main Emissions Controller. However, Outpost can't perform fine-grained sanity checks for voted LP tokens. -Same restrictions as on the Hub are applied, like voting every 10 days for up to 5 pools at once. +Users can vote up to five pools only once per epoch. +Once votes are cast they can't be changed until the next epoch. The contract composes a special internal IBC message to the Hub with the user's vote. If sanity checks passed on the Hub, the vote is accepted. In case of IBC failure or timeouts, the user can try to vote again. 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 e6ca4ed..3644735 100644 --- a/contracts/emissions_controller_outpost/tests/emissions_controller_outpost_integration.rs +++ b/contracts/emissions_controller_outpost/tests/emissions_controller_outpost_integration.rs @@ -6,11 +6,11 @@ use cw_utils::PaymentError; use astroport_emissions_controller_outpost::error::ContractError; use astroport_governance::assembly::ProposalVoteOption; -use astroport_governance::emissions_controller; use astroport_governance::emissions_controller::consts::{EPOCH_LENGTH, IBC_TIMEOUT}; use astroport_governance::emissions_controller::msg::{ExecuteMsg, VxAstroIbcMsg}; use astroport_governance::emissions_controller::outpost::UserIbcError; use astroport_governance::voting_escrow::LockInfoResponse; +use astroport_governance::{emissions_controller, voting_escrow}; use astroport_voting_escrow::state::UNLOCK_PERIOD; use crate::common::helper::{get_epoch_start, ControllerHelper}; @@ -490,6 +490,26 @@ fn test_voting() { ); } +#[test] +fn test_privileged_list_disabled() { + let mut helper = ControllerHelper::new(); + let owner = helper.owner.clone(); + let user = helper.app.api().addr_make("user"); + + // Must fail to deserialize outpost controller Config into Hub's controller Config + helper + .app + .execute_contract( + owner.clone(), + helper.vxastro.clone(), + &voting_escrow::ExecuteMsg::SetPrivilegedList { + list: vec![user.to_string()], + }, + &[], + ) + .unwrap_err(); +} + #[test] fn test_unlock_and_withdraw() { let mut helper = ControllerHelper::new(); diff --git a/contracts/voting_escrow/Cargo.toml b/contracts/voting_escrow/Cargo.toml index 73828fa..af60d2c 100644 --- a/contracts/voting_escrow/Cargo.toml +++ b/contracts/voting_escrow/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-voting-escrow" -version = "1.0.0" +version = "1.1.0" authors = ["Astroport"] edition = "2021" description = "Astroport Vote Escrowed xASTRO (vxASTRO)" diff --git a/contracts/voting_escrow/src/contract.rs b/contracts/voting_escrow/src/contract.rs index d47eddf..f02bfc8 100644 --- a/contracts/voting_escrow/src/contract.rs +++ b/contracts/voting_escrow/src/contract.rs @@ -2,8 +2,8 @@ use astroport::asset::{addr_opt_validate, validate_native_denom}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, coins, ensure, to_json_binary, wasm_execute, BankMsg, Binary, Deps, DepsMut, Empty, Env, - MessageInfo, Response, StdError, StdResult, Uint128, + attr, coins, ensure, ensure_eq, to_json_binary, wasm_execute, BankMsg, Binary, CosmosMsg, Deps, + DepsMut, Empty, Env, MessageInfo, Response, StdError, StdResult, Uint128, }; use cw2::set_contract_version; use cw20::{BalanceResponse, Logo, LogoInfo, MarketingInfoResponse, TokenInfoResponse}; @@ -17,7 +17,7 @@ use astroport_governance::voting_escrow::{ }; use crate::error::ContractError; -use crate::state::{get_total_vp, Lock, CONFIG}; +use crate::state::{get_total_vp, Lock, CONFIG, PRIVILEGED}; /// Contract name that is used for migration. pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -74,6 +74,8 @@ pub fn instantiate( TOKEN_INFO.save(deps.storage, &data)?; + PRIVILEGED.save(deps.storage, &vec![])?; + Ok(Response::default()) } @@ -138,6 +140,44 @@ pub fn execute( attr("unlock_time", unlock_time.to_string()), ])) } + ExecuteMsg::InstantUnlock { amount } => { + let privileged = PRIVILEGED.load(deps.storage)?; + ensure!( + privileged.contains(&info.sender), + ContractError::Unauthorized {} + ); + + let mut position = Lock::load(deps.storage, env.block.time.seconds(), &info.sender)?; + position.instant_unlock(deps.storage, amount)?; + + // Update user votes in emissions controller + let config = CONFIG.load(deps.storage)?; + let update_votes_msg: CosmosMsg = wasm_execute( + config.emissions_controller, + &emissions_controller::msg::ExecuteMsg::::UpdateUserVotes { + user: info.sender.to_string(), + // In this context, we don't need confirmation from emissions controller + // as xASTRO is instantly withdrawn + is_unlock: false, + }, + vec![], + )? + .into(); + + let send_msg: CosmosMsg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins(amount.u128(), config.deposit_denom), + } + .into(); + + Ok(Response::default() + .add_messages([update_votes_msg, send_msg]) + .add_attributes([ + attr("action", "instant_unlock"), + attr("receiver", info.sender), + attr("unlocked_amount", amount), + ])) + } ExecuteMsg::Relock {} => { let mut position = Lock::load(deps.storage, env.block.time.seconds(), &info.sender)?; position.relock(deps.storage)?; @@ -201,6 +241,29 @@ pub fn execute( attr("withdrawn_amount", amount), ])) } + ExecuteMsg::SetPrivilegedList { list } => { + let config = CONFIG.load(deps.storage)?; + + // Query result deserialization into hub::Config + // ensures we can call this endpoint only on the Hub + let emissions_owner = deps + .querier + .query_wasm_smart::( + &config.emissions_controller, + &emissions_controller::hub::QueryMsg::Config {}, + )? + .owner; + ensure_eq!(info.sender, emissions_owner, ContractError::Unauthorized {}); + + let privileged = list + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + PRIVILEGED.save(deps.storage, &privileged)?; + + Ok(Response::default().add_attribute("action", "set_privileged_list")) + } ExecuteMsg::UpdateMarketing { project, description, @@ -235,6 +298,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } QueryMsg::TokenInfo {} => to_json_binary(&query_token_info(deps, env)?), QueryMsg::MarketingInfo {} => to_json_binary(&query_marketing_info(deps)?), + QueryMsg::PrivilegedList {} => to_json_binary(&PRIVILEGED.load(deps.storage)?), } } diff --git a/contracts/voting_escrow/src/error.rs b/contracts/voting_escrow/src/error.rs index 96b25a7..47d91a0 100644 --- a/contracts/voting_escrow/src/error.rs +++ b/contracts/voting_escrow/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{OverflowError, StdError}; use cw20_base::ContractError as CW20Error; use cw_utils::PaymentError; use thiserror::Error; @@ -12,6 +12,9 @@ pub enum ContractError { #[error("{0}")] PaymentError(#[from] PaymentError), + #[error("{0}")] + OverflowError(#[from] OverflowError), + #[error("{0}")] Cw20Base(#[from] CW20Error), @@ -32,4 +35,7 @@ pub enum ContractError { #[error("Hub has not yet confirmed the unlock")] HubNotConfirmed {}, + + #[error("Failed to migrate contract")] + MigrationError {}, } diff --git a/contracts/voting_escrow/src/lib.rs b/contracts/voting_escrow/src/lib.rs index 3d3e89c..f5888f1 100644 --- a/contracts/voting_escrow/src/lib.rs +++ b/contracts/voting_escrow/src/lib.rs @@ -1,3 +1,4 @@ pub mod contract; pub mod error; +pub mod migration; pub mod state; diff --git a/contracts/voting_escrow/src/migration.rs b/contracts/voting_escrow/src/migration.rs new file mode 100644 index 0000000..fa228cd --- /dev/null +++ b/contracts/voting_escrow/src/migration.rs @@ -0,0 +1,28 @@ +#![cfg(not(tarpaulin_include))] + +use cosmwasm_std::{DepsMut, Empty, Env, Response}; +use cw2::{get_contract_version, set_contract_version}; + +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::error::ContractError; + +#[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/voting_escrow/src/state.rs b/contracts/voting_escrow/src/state.rs index f335751..63b579b 100644 --- a/contracts/voting_escrow/src/state.rs +++ b/contracts/voting_escrow/src/state.rs @@ -10,6 +10,8 @@ pub const UNLOCK_PERIOD: u64 = 86400 * 14; // 2 weeks /// Stores the contract config at the given key pub const CONFIG: Item = Item::new("config"); +/// Keeps the list of addresses that are allowed to instantly unlock xASTRO +pub const PRIVILEGED: Item> = Item::new("privileged"); fn default_addr() -> Addr { Addr::unchecked("") @@ -105,6 +107,22 @@ impl Lock { Ok(end) } + pub fn instant_unlock( + &mut self, + storage: &mut dyn Storage, + amount: Uint128, + ) -> Result<(), ContractError> { + self.amount = self.amount.checked_sub(amount)?; + LOCKED.save(storage, &self.user, self, self.block_time)?; + + // Remove unlocked voting power from the total + TOTAL_POWER.update(storage, self.block_time, |total| -> StdResult<_> { + Ok(total.unwrap_or_default().checked_sub(amount)?) + })?; + + Ok(()) + } + pub fn confirm_unlock(&mut self, storage: &mut dyn Storage) -> StdResult<()> { // If for some reason the unlock status is not set, // we skip it silently so relayer can finish IBC transaction. diff --git a/contracts/voting_escrow/tests/helper.rs b/contracts/voting_escrow/tests/helper.rs index 73257b2..c8ee963 100644 --- a/contracts/voting_escrow/tests/helper.rs +++ b/contracts/voting_escrow/tests/helper.rs @@ -46,7 +46,6 @@ fn mock_emissions_controller() -> Box> { pub struct EscrowHelper { pub app: BasicApp, pub owner: Addr, - pub xastro_denom: String, pub vxastro_contract: Addr, pub emissions_controller: Addr, } @@ -91,7 +90,6 @@ impl EscrowHelper { Self { app, owner, - xastro_denom: xastro_denom.to_string(), vxastro_contract, emissions_controller: mocked_emission_controller, } diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 5584444..0ef6c01 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "4.0.0" +version = "4.1.0" authors = ["Astroport"] edition = "2021" description = "Astroport Governance common types, queriers and other utils" diff --git a/packages/astroport-governance/src/emissions_controller/consts.rs b/packages/astroport-governance/src/emissions_controller/consts.rs index 9ebd98a..e9448e6 100644 --- a/packages/astroport-governance/src/emissions_controller/consts.rs +++ b/packages/astroport-governance/src/emissions_controller/consts.rs @@ -17,8 +17,6 @@ pub const POOL_NUMBER_LIMIT: RangeInclusive = 1..=10; pub const MAX_POOLS_TO_VOTE: usize = 5; /// Max items per page in queries pub const MAX_PAGE_LIMIT: u8 = 50; -/// User can vote once every 10 days -pub const VOTE_COOLDOWN: u64 = DAY * 10; /// vxASTRO IBC version pub const IBC_APP_VERSION: &str = "vxastro-ibc-v1"; /// IBC ordering diff --git a/packages/astroport-governance/src/voting_escrow.rs b/packages/astroport-governance/src/voting_escrow.rs index 8cea663..1d02856 100644 --- a/packages/astroport-governance/src/voting_escrow.rs +++ b/packages/astroport-governance/src/voting_escrow.rs @@ -33,6 +33,10 @@ pub enum ExecuteMsg { Lock { receiver: Option }, /// Unlock xASTRO from the vxASTRO contract Unlock {}, + /// Instantly unlock xASTRO from the vxASTRO contract without waiting period. + /// Only privileged addresses can call this. + /// NOTE: due to async nature of IBC this feature will be enabled only on the hub. + InstantUnlock { amount: Uint128 }, /// Cancel unlocking Relock {}, /// Permissioned to the Emissions Controller contract. @@ -46,6 +50,10 @@ pub enum ExecuteMsg { ForceRelock { user: String }, /// Withdraw xASTRO from the vxASTRO contract Withdraw {}, + /// Set the list of addresses that allowed to instantly unlock xASTRO. + /// Only contract owner can call this. + /// NOTE: due to async nature of IBC this feature will be enabled only on the hub. + SetPrivilegedList { list: Vec }, /// Update the marketing info for the vxASTRO contract UpdateMarketing { /// A URL pointing to the project behind this token @@ -85,6 +93,9 @@ pub enum QueryMsg { /// Return the vxASTRO contract configuration #[returns(Config)] Config {}, + /// Return the list of addresses that are allowed to instantly unlock xASTRO + #[returns(Vec)] + PrivilegedList {}, } /// This structure stores the main parameters for the voting escrow contract. diff --git a/schemas/astroport-emissions-controller/astroport-emissions-controller.json b/schemas/astroport-emissions-controller/astroport-emissions-controller.json index 119fbba..6e9a28b 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.0", + "contract_version": "1.0.1", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/astroport-voting-escrow/astroport-voting-escrow.json b/schemas/astroport-voting-escrow/astroport-voting-escrow.json index 00a938e..33c2672 100644 --- a/schemas/astroport-voting-escrow/astroport-voting-escrow.json +++ b/schemas/astroport-voting-escrow/astroport-voting-escrow.json @@ -1,6 +1,6 @@ { "contract_name": "astroport-voting-escrow", - "contract_version": "1.0.0", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -180,6 +180,28 @@ }, "additionalProperties": false }, + { + "description": "Instantly unlock xASTRO from the vxASTRO contract without waiting period. Only privileged addresses can call this. NOTE: due to async nature of IBC this feature will be enabled only on the hub.", + "type": "object", + "required": [ + "instant_unlock" + ], + "properties": { + "instant_unlock": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Cancel unlocking", "type": "object", @@ -252,6 +274,31 @@ }, "additionalProperties": false }, + { + "description": "Set the list of addresses that allowed to instantly unlock xASTRO. Only contract owner can call this. NOTE: due to async nature of IBC this feature will be enabled only on the hub.", + "type": "object", + "required": [ + "set_privileged_list" + ], + "properties": { + "set_privileged_list": { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Update the marketing info for the vxASTRO contract", "type": "object", @@ -289,7 +336,13 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } }, "query": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -435,6 +488,20 @@ } }, "additionalProperties": false + }, + { + "description": "Return the list of addresses that are allowed to instantly unlock xASTRO", + "type": "object", + "required": [ + "privileged_list" + ], + "properties": { + "privileged_list": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -622,6 +689,20 @@ } } }, + "privileged_list": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + }, + "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" + } + } + }, "token_info": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "TokenInfoResponse", diff --git a/schemas/astroport-voting-escrow/raw/execute.json b/schemas/astroport-voting-escrow/raw/execute.json index 45b60ac..bcf505a 100644 --- a/schemas/astroport-voting-escrow/raw/execute.json +++ b/schemas/astroport-voting-escrow/raw/execute.json @@ -39,6 +39,28 @@ }, "additionalProperties": false }, + { + "description": "Instantly unlock xASTRO from the vxASTRO contract without waiting period. Only privileged addresses can call this. NOTE: due to async nature of IBC this feature will be enabled only on the hub.", + "type": "object", + "required": [ + "instant_unlock" + ], + "properties": { + "instant_unlock": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Cancel unlocking", "type": "object", @@ -111,6 +133,31 @@ }, "additionalProperties": false }, + { + "description": "Set the list of addresses that allowed to instantly unlock xASTRO. Only contract owner can call this. NOTE: due to async nature of IBC this feature will be enabled only on the hub.", + "type": "object", + "required": [ + "set_privileged_list" + ], + "properties": { + "set_privileged_list": { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Update the marketing info for the vxASTRO contract", "type": "object", @@ -148,5 +195,11 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } } diff --git a/schemas/astroport-voting-escrow/raw/query.json b/schemas/astroport-voting-escrow/raw/query.json index 81a71a7..56ea9f4 100644 --- a/schemas/astroport-voting-escrow/raw/query.json +++ b/schemas/astroport-voting-escrow/raw/query.json @@ -142,6 +142,20 @@ } }, "additionalProperties": false + }, + { + "description": "Return the list of addresses that are allowed to instantly unlock xASTRO", + "type": "object", + "required": [ + "privileged_list" + ], + "properties": { + "privileged_list": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] } diff --git a/schemas/astroport-voting-escrow/raw/response_to_privileged_list.json b/schemas/astroport-voting-escrow/raw/response_to_privileged_list.json new file mode 100644 index 0000000..bf06b01 --- /dev/null +++ b/schemas/astroport-voting-escrow/raw/response_to_privileged_list.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + }, + "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" + } + } +} diff --git a/scripts/build_release.sh b/scripts/build_release.sh index 9724903..7750167 100755 --- a/scripts/build_release.sh +++ b/scripts/build_release.sh @@ -6,6 +6,6 @@ set -o pipefail projectPath=$(cd "$(dirname "${0}")" && cd ../ && pwd) docker run --rm -v "$projectPath":/code \ - --mount type=volume,source="$(basename "$projectPath")_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer:0.15.1 \ No newline at end of file + --mount type=volume,source="$(basename "$projectPath")_cache",target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/optimizer:0.15.1 \ No newline at end of file