diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ab984..770b7e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ -## 0.2.0 +## 0.3.0 +Astroport support for collector and staking contracts. +## 0.2.0 Columbus-5 upgrade compatible version release. diff --git a/contracts/airdrop/Cargo.toml b/contracts/airdrop/Cargo.toml index 60d62b3..f03fbfb 100644 --- a/contracts/airdrop/Cargo.toml +++ b/contracts/airdrop/Cargo.toml @@ -34,7 +34,7 @@ overflow-checks = true backtraces = ["cosmwasm-std/backtraces"] [dependencies] -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } cosmwasm-std = { version = "0.16.0" } cosmwasm-storage = { version = "0.16.0" } cw20 = { version = "0.8.0" } diff --git a/contracts/collector/Cargo.toml b/contracts/collector/Cargo.toml index 067f110..240e563 100644 --- a/contracts/collector/Cargo.toml +++ b/contracts/collector/Cargo.toml @@ -37,9 +37,9 @@ backtraces = ["cosmwasm-std/backtraces"] cw20 = { version = "0.8.0" } cosmwasm-std = { version = "0.16.0" } cosmwasm-storage = { version = "0.16.0" } -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } terra-cosmwasm = "2.2.0" -terraswap = { version = "2.4.0" } +astroport = "0.3.1" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/collector/schema/config_response.json b/contracts/collector/schema/config_response.json index d90a5f3..ca829f4 100644 --- a/contracts/collector/schema/config_response.json +++ b/contracts/collector/schema/config_response.json @@ -4,26 +4,32 @@ "type": "object", "required": [ "anchor_token", - "distributor_contract", + "astroport_factory", "gov_contract", - "reward_factor", - "terraswap_factory" + "reward_factor" ], "properties": { "anchor_token": { "type": "string" }, - "distributor_contract": { + "astroport_factory": { "type": "string" }, "gov_contract": { "type": "string" }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "reward_factor": { "$ref": "#/definitions/Decimal" - }, - "terraswap_factory": { - "type": "string" } }, "definitions": { diff --git a/contracts/collector/schema/execute_msg.json b/contracts/collector/schema/execute_msg.json index a378a88..5252e5f 100644 --- a/contracts/collector/schema/execute_msg.json +++ b/contracts/collector/schema/execute_msg.json @@ -1,9 +1,9 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", - "anyOf": [ + "oneOf": [ { - "description": "Update config interface to enable reward_factor update", + "description": "Update config interface to enable reward_factor update ## NOTE: for updating `max spread` it should be either (true, none) or (true, \"0.1\") if we do not want to update it it should be (false, none)", "type": "object", "required": [ "update_config" @@ -11,7 +11,42 @@ "properties": { "update_config": { "type": "object", + "required": [ + "max_spread" + ], "properties": { + "astroport_factory": { + "type": [ + "string", + "null" + ] + }, + "gov_contract": { + "type": [ + "string", + "null" + ] + }, + "max_spread": { + "type": "array", + "items": [ + { + "type": "boolean" + }, + { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + }, "reward_factor": { "anyOf": [ { diff --git a/contracts/collector/schema/instantiate_msg.json b/contracts/collector/schema/instantiate_msg.json index dc63611..d649f7a 100644 --- a/contracts/collector/schema/instantiate_msg.json +++ b/contracts/collector/schema/instantiate_msg.json @@ -4,26 +4,32 @@ "type": "object", "required": [ "anchor_token", - "distributor_contract", + "astroport_factory", "gov_contract", - "reward_factor", - "terraswap_factory" + "reward_factor" ], "properties": { "anchor_token": { "type": "string" }, - "distributor_contract": { + "astroport_factory": { "type": "string" }, "gov_contract": { "type": "string" }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "reward_factor": { "$ref": "#/definitions/Decimal" - }, - "terraswap_factory": { - "type": "string" } }, "definitions": { diff --git a/contracts/collector/schema/query_msg.json b/contracts/collector/schema/query_msg.json index 6392c2a..3df1ec1 100644 --- a/contracts/collector/schema/query_msg.json +++ b/contracts/collector/schema/query_msg.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", - "anyOf": [ + "oneOf": [ { "type": "object", "required": [ diff --git a/contracts/collector/src/contract.rs b/contracts/collector/src/contract.rs index 9b9e068..ac21c11 100644 --- a/contracts/collector/src/contract.rs +++ b/contracts/collector/src/contract.rs @@ -2,17 +2,18 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, to_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, - Response, StdError, StdResult, SubMsg, WasmMsg, + attr, to_binary, Addr, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, + Reply, Response, StdError, StdResult, SubMsg, WasmMsg, }; use crate::state::{read_config, store_config, Config}; +use crate::migration::migrate_config; use anchor_token::collector::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::pair::ExecuteMsg as AstroportExecuteMsg; +use astroport::querier::{query_balance, query_pair_info, query_token_balance}; use cw20::Cw20ExecuteMsg; -use terraswap::asset::{Asset, AssetInfo, PairInfo}; -use terraswap::pair::ExecuteMsg as TerraswapExecuteMsg; -use terraswap::querier::{query_balance, query_pair_info, query_token_balance}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -25,10 +26,10 @@ pub fn instantiate( deps.storage, &Config { gov_contract: deps.api.addr_canonicalize(&msg.gov_contract)?, - terraswap_factory: deps.api.addr_canonicalize(&msg.terraswap_factory)?, + astroport_factory: deps.api.addr_canonicalize(&msg.astroport_factory)?, anchor_token: deps.api.addr_canonicalize(&msg.anchor_token)?, - distributor_contract: deps.api.addr_canonicalize(&msg.distributor_contract)?, reward_factor: msg.reward_factor, + max_spread: msg.max_spread, }, )?; @@ -38,7 +39,19 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { match msg { - ExecuteMsg::UpdateConfig { reward_factor } => update_config(deps, info, reward_factor), + ExecuteMsg::UpdateConfig { + reward_factor, + gov_contract, + astroport_factory, + max_spread, + } => update_config( + deps, + info, + reward_factor, + gov_contract, + astroport_factory, + max_spread, + ), ExecuteMsg::Sweep { denom } => sweep(deps, env, denom), } } @@ -47,6 +60,9 @@ pub fn update_config( deps: DepsMut, info: MessageInfo, reward_factor: Option, + gov_contract: Option, + astroport_factory: Option, + max_spread: (bool, Option), ) -> StdResult { let mut config: Config = read_config(deps.storage)?; if deps.api.addr_canonicalize(info.sender.as_str())? != config.gov_contract { @@ -57,6 +73,17 @@ pub fn update_config( config.reward_factor = reward_factor; } + if let Some(gov_contract) = gov_contract { + config.gov_contract = deps.api.addr_canonicalize(gov_contract.as_str())?; + } + if let Some(astroport_factory) = astroport_factory { + config.astroport_factory = deps.api.addr_canonicalize(astroport_factory.as_str())?; + } + + if max_spread.0 { + config.max_spread = max_spread.1 + } + store_config(deps.storage, &config)?; Ok(Response::default()) } @@ -70,17 +97,17 @@ const SWEEP_REPLY_ID: u64 = 1; pub fn sweep(deps: DepsMut, env: Env, denom: String) -> StdResult { let config: Config = read_config(deps.storage)?; let anchor_token = deps.api.addr_humanize(&config.anchor_token)?; - let terraswap_factory_addr = deps.api.addr_humanize(&config.terraswap_factory)?; + let astroport_factory_addr = deps.api.addr_humanize(&config.astroport_factory)?; let pair_info: PairInfo = query_pair_info( &deps.querier, - terraswap_factory_addr, + astroport_factory_addr, &[ AssetInfo::NativeToken { denom: denom.to_string(), }, AssetInfo::Token { - contract_addr: anchor_token.to_string(), + contract_addr: Addr::unchecked(anchor_token), }, ], )?; @@ -99,13 +126,13 @@ pub fn sweep(deps: DepsMut, env: Env, denom: String) -> StdResult { Ok(Response::new() .add_submessage(SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.contract_addr, - msg: to_binary(&TerraswapExecuteMsg::Swap { + contract_addr: pair_info.contract_addr.into_string(), + msg: to_binary(&AstroportExecuteMsg::Swap { offer_asset: Asset { amount, ..swap_asset }, - max_spread: None, + max_spread: config.max_spread, belief_price: None, to: None, })?, @@ -160,14 +187,11 @@ pub fn distribute(deps: DepsMut, env: Env) -> StdResult { })); } + // burn the left amount if !left_amount.is_zero() { messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: deps.api.addr_humanize(&config.anchor_token)?.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: deps - .api - .addr_humanize(&config.distributor_contract)? - .to_string(), + msg: to_binary(&Cw20ExecuteMsg::Burn { amount: left_amount, })?, funds: vec![], @@ -192,22 +216,26 @@ pub fn query_config(deps: Deps) -> StdResult { let state = read_config(deps.storage)?; let resp = ConfigResponse { gov_contract: deps.api.addr_humanize(&state.gov_contract)?.to_string(), - terraswap_factory: deps + astroport_factory: deps .api - .addr_humanize(&state.terraswap_factory)? + .addr_humanize(&state.astroport_factory)? .to_string(), anchor_token: deps.api.addr_humanize(&state.anchor_token)?.to_string(), - distributor_contract: deps - .api - .addr_humanize(&state.distributor_contract)? - .to_string(), reward_factor: state.reward_factor, + max_spread: state.max_spread, }; Ok(resp) } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + //migrate config + migrate_config( + deps.storage, + deps.api.addr_canonicalize(&msg.astroport_factory)?, + msg.max_spread, + )?; + Ok(Response::default()) } diff --git a/contracts/collector/src/lib.rs b/contracts/collector/src/lib.rs index 68e2087..c89e271 100644 --- a/contracts/collector/src/lib.rs +++ b/contracts/collector/src/lib.rs @@ -1,4 +1,5 @@ pub mod contract; +pub mod migration; pub mod state; #[cfg(test)] diff --git a/contracts/collector/src/migration.rs b/contracts/collector/src/migration.rs new file mode 100644 index 0000000..cbfb7cb --- /dev/null +++ b/contracts/collector/src/migration.rs @@ -0,0 +1,38 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::state::{store_config, Config, KEY_CONFIG}; +use cosmwasm_std::{CanonicalAddr, Decimal, StdResult, Storage}; +use cosmwasm_storage::ReadonlySingleton; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct LegacyConfig { + pub gov_contract: CanonicalAddr, // collected rewards receiver + pub terraswap_factory: CanonicalAddr, // astroport factory contract + pub anchor_token: CanonicalAddr, // anchor token address + pub distributor_contract: CanonicalAddr, // distributor contract to sent back rewards + pub reward_factor: Decimal, // reward distribution rate to gov contract, left rewards sent back to distributor contract +} + +fn read_legacy_config(storage: &dyn Storage) -> StdResult { + ReadonlySingleton::new(storage, KEY_CONFIG).load() +} + +pub fn migrate_config( + storage: &mut dyn Storage, + astroport_factory: CanonicalAddr, + max_spread: Decimal, +) -> StdResult<()> { + let legacy_config: LegacyConfig = read_legacy_config(storage)?; + + store_config( + storage, + &Config { + gov_contract: legacy_config.gov_contract, + astroport_factory, + anchor_token: legacy_config.anchor_token, + reward_factor: legacy_config.reward_factor, + max_spread: Some(max_spread), + }, + ) +} diff --git a/contracts/collector/src/mock_querier.rs b/contracts/collector/src/mock_querier.rs index f57fac1..0bb3f7e 100644 --- a/contracts/collector/src/mock_querier.rs +++ b/contracts/collector/src/mock_querier.rs @@ -3,15 +3,16 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ - from_binary, from_slice, to_binary, Coin, ContractResult, Decimal, OwnedDeps, Querier, + from_binary, from_slice, to_binary, Addr, Coin, ContractResult, Decimal, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, }; use cw20::{BalanceResponse as Cw20BalanceResponse, Cw20QueryMsg}; use std::collections::HashMap; +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::factory::PairType; use terra_cosmwasm::{TaxCapResponse, TaxRateResponse, TerraQuery, TerraQueryWrapper, TerraRoute}; -use terraswap::asset::{AssetInfo, PairInfo}; /// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies /// this uses our CustomQuerier. @@ -32,7 +33,7 @@ pub struct WasmMockQuerier { base: MockQuerier, token_querier: TokenQuerier, tax_querier: TaxQuerier, - terraswap_factory_querier: TerraswapFactoryQuerier, + astroport_factory_querier: AstroportFactoryQuerier, } #[derive(Clone, Default)] @@ -89,13 +90,13 @@ pub(crate) fn caps_to_map(caps: &[(&String, &Uint128)]) -> HashMap, } -impl TerraswapFactoryQuerier { +impl AstroportFactoryQuerier { pub fn new(pairs: &[(&String, &String)]) -> Self { - TerraswapFactoryQuerier { + AstroportFactoryQuerier { pairs: pairs_to_map(pairs), } } @@ -162,10 +163,10 @@ impl WasmMockQuerier { QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => match from_binary(msg) { Ok(QueryMsg::Pair { asset_infos }) => { let key = asset_infos[0].to_string() + asset_infos[1].to_string().as_str(); - match self.terraswap_factory_querier.pairs.get(&key) { + match self.astroport_factory_querier.pairs.get(&key) { Some(v) => SystemResult::Ok(ContractResult::from(to_binary(&PairInfo { - contract_addr: v.to_string(), - liquidity_token: "liquidity".to_string(), + contract_addr: Addr::unchecked(v.to_string()), + liquidity_token: Addr::unchecked("liquidity".to_string()), asset_infos: [ AssetInfo::NativeToken { denom: "uusd".to_string(), @@ -174,6 +175,7 @@ impl WasmMockQuerier { denom: "uusd".to_string(), }, ], + pair_type: PairType::Stable {}, }))), None => SystemResult::Err(SystemError::InvalidRequest { error: "No pair info exists".to_string(), @@ -227,7 +229,7 @@ impl WasmMockQuerier { base, token_querier: TokenQuerier::default(), tax_querier: TaxQuerier::default(), - terraswap_factory_querier: TerraswapFactoryQuerier::default(), + astroport_factory_querier: AstroportFactoryQuerier::default(), } } @@ -241,8 +243,8 @@ impl WasmMockQuerier { self.tax_querier = TaxQuerier::new(rate, caps); } - // configure the terraswap pair - pub fn with_terraswap_pairs(&mut self, pairs: &[(&String, &String)]) { - self.terraswap_factory_querier = TerraswapFactoryQuerier::new(pairs); + // configure the astroport pair + pub fn with_astroport_pairs(&mut self, pairs: &[(&String, &String)]) { + self.astroport_factory_querier = AstroportFactoryQuerier::new(pairs); } } diff --git a/contracts/collector/src/state.rs b/contracts/collector/src/state.rs index e0d7087..9de01a8 100644 --- a/contracts/collector/src/state.rs +++ b/contracts/collector/src/state.rs @@ -4,15 +4,15 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, Decimal, StdResult, Storage}; use cosmwasm_storage::{singleton, singleton_read}; -static KEY_CONFIG: &[u8] = b"config"; +pub static KEY_CONFIG: &[u8] = b"config"; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Config { - pub gov_contract: CanonicalAddr, // collected rewards receiver - pub terraswap_factory: CanonicalAddr, // terraswap factory contract - pub anchor_token: CanonicalAddr, // anchor token address - pub distributor_contract: CanonicalAddr, // distributor contract to sent back rewards + pub gov_contract: CanonicalAddr, // collected rewards receiver + pub astroport_factory: CanonicalAddr, // astroport factory contract + pub anchor_token: CanonicalAddr, // anchor token address pub reward_factor: Decimal, // reward distribution rate to gov contract, left rewards sent back to distributor contract + pub max_spread: Option, // max spread for buybacks } pub fn store_config(storage: &mut dyn Storage, config: &Config) -> StdResult<()> { diff --git a/contracts/collector/src/testing.rs b/contracts/collector/src/testing.rs index f2b3605..5fc2996 100644 --- a/contracts/collector/src/testing.rs +++ b/contracts/collector/src/testing.rs @@ -1,25 +1,25 @@ use crate::contract::{execute, instantiate, query_config, reply}; use crate::mock_querier::mock_dependencies; use anchor_token::collector::{ConfigResponse, ExecuteMsg, InstantiateMsg}; +use astroport::asset::{Asset, AssetInfo}; +use astroport::pair::ExecuteMsg as AstroportExecuteMsg; use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ to_binary, Coin, ContractResult, CosmosMsg, Decimal, Reply, ReplyOn, StdError, SubMsg, SubMsgExecutionResponse, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; -use terraswap::asset::{Asset, AssetInfo}; -use terraswap::pair::ExecuteMsg as TerraswapExecuteMsg; #[test] fn proper_initialization() { let mut deps = mock_dependencies(&[]); let msg = InstantiateMsg { - terraswap_factory: "terraswapfactory".to_string(), + astroport_factory: "astroportfactory".to_string(), gov_contract: "gov".to_string(), anchor_token: "tokenANC".to_string(), - distributor_contract: "distributor".to_string(), reward_factor: Decimal::percent(90), + max_spread: Default::default(), }; let info = mock_info("addr0000", &[]); @@ -29,7 +29,7 @@ fn proper_initialization() { // it worked, let's query the state let config: ConfigResponse = query_config(deps.as_ref()).unwrap(); - assert_eq!("terraswapfactory", config.terraswap_factory.as_str()); + assert_eq!("astroportfactory", config.astroport_factory.as_str()); } #[test] @@ -37,11 +37,11 @@ fn update_config() { let mut deps = mock_dependencies(&[]); let msg = InstantiateMsg { - terraswap_factory: "terraswapfactory".to_string(), + astroport_factory: "astroportfactory".to_string(), gov_contract: "gov".to_string(), anchor_token: "tokenANC".to_string(), - distributor_contract: "distributor".to_string(), reward_factor: Decimal::percent(90), + max_spread: Default::default(), }; let info = mock_info("addr0000", &[]); @@ -51,6 +51,9 @@ fn update_config() { let info = mock_info("gov", &[]); let msg = ExecuteMsg::UpdateConfig { reward_factor: Some(Decimal::percent(80)), + gov_contract: Some("new_gov".to_string()), + astroport_factory: Some("new_astroport_factory".to_string()), + max_spread: (true, Some(Decimal::percent(10))), }; let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); @@ -59,11 +62,36 @@ fn update_config() { // it worked, let's query the state let value = query_config(deps.as_ref()).unwrap(); assert_eq!(Decimal::percent(80), value.reward_factor); + assert_eq!(value.astroport_factory, "new_astroport_factory".to_string()); + assert_eq!(value.gov_contract, "new_gov".to_string()); + assert_eq!(value.max_spread, Some(Decimal::percent(10))); + + // test max spread update + let info = mock_info("new_gov", &[]); + let msg = ExecuteMsg::UpdateConfig { + reward_factor: None, + gov_contract: None, + astroport_factory: None, + max_spread: (true, None), + }; + + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let value = query_config(deps.as_ref()).unwrap(); + assert_eq!(Decimal::percent(80), value.reward_factor); + assert_eq!(value.astroport_factory, "new_astroport_factory".to_string()); + assert_eq!(value.gov_contract, "new_gov".to_string()); + assert_eq!(value.max_spread, None); // Unauthorized err let info = mock_info("addr0000", &[]); let msg = ExecuteMsg::UpdateConfig { reward_factor: None, + gov_contract: Some("new_gov".to_string()), + astroport_factory: Some("new_astroport_factory".to_string()), + max_spread: (false, None), }; let res = execute(deps.as_mut(), mock_env(), info, msg); @@ -86,14 +114,14 @@ fn test_sweep() { ); deps.querier - .with_terraswap_pairs(&[(&"uusdtokenANC".to_string(), &"pairANC".to_string())]); + .with_astroport_pairs(&[(&"uusdtokenANC".to_string(), &"pairANC".to_string())]); let msg = InstantiateMsg { - terraswap_factory: "terraswapfactory".to_string(), + astroport_factory: "astroportfactory".to_string(), gov_contract: "gov".to_string(), anchor_token: "tokenANC".to_string(), - distributor_contract: "distributor".to_string(), reward_factor: Decimal::percent(90), + max_spread: Some(Decimal::percent(10)), }; let info = mock_info("addr0000", &[]); @@ -111,14 +139,14 @@ fn test_sweep() { vec![SubMsg { msg: WasmMsg::Execute { contract_addr: "pairANC".to_string(), - msg: to_binary(&TerraswapExecuteMsg::Swap { + msg: to_binary(&AstroportExecuteMsg::Swap { offer_asset: Asset { info: AssetInfo::NativeToken { denom: "uusd".to_string() }, amount: Uint128::from(99u128), }, - max_spread: None, + max_spread: Some(Decimal::percent(10)), belief_price: None, to: None, }) @@ -146,11 +174,11 @@ fn test_distribute() { )]); let msg = InstantiateMsg { - terraswap_factory: "terraswapfactory".to_string(), + astroport_factory: "astroportfactory".to_string(), gov_contract: "gov".to_string(), anchor_token: "tokenANC".to_string(), - distributor_contract: "distributor".to_string(), reward_factor: Decimal::percent(90), + max_spread: Some(Decimal::percent(10)), }; let info = mock_info("addr0000", &[]); @@ -179,8 +207,7 @@ fn test_distribute() { })), SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "tokenANC".to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: "distributor".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Burn { amount: Uint128::from(10u128), }) .unwrap(), diff --git a/contracts/community/Cargo.toml b/contracts/community/Cargo.toml index 847e810..9950cbf 100644 --- a/contracts/community/Cargo.toml +++ b/contracts/community/Cargo.toml @@ -37,7 +37,7 @@ backtraces = ["cosmwasm-std/backtraces"] cw20 = { version = "0.8.0" } cosmwasm-std = { version = "0.16.0" } cosmwasm-storage = { version = "0.16.0" } -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/distributor/Cargo.toml b/contracts/distributor/Cargo.toml index 84abef6..8176096 100644 --- a/contracts/distributor/Cargo.toml +++ b/contracts/distributor/Cargo.toml @@ -37,7 +37,7 @@ backtraces = ["cosmwasm-std/backtraces"] cw20 = { version = "0.8.0" } cosmwasm-std = { version = "0.16.0" } cosmwasm-storage = { version = "0.16.0" } -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/gov/Cargo.toml b/contracts/gov/Cargo.toml index 72f4700..4426d09 100644 --- a/contracts/gov/Cargo.toml +++ b/contracts/gov/Cargo.toml @@ -38,10 +38,10 @@ backtraces = ["cosmwasm-std/backtraces"] cw20 = { version = "0.8.0" } cosmwasm-std = { version = "0.16.0", features = ["iterator"] } cosmwasm-storage = { version = "0.16.0", features = ["iterator"] } -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -terraswap = { version = "2.4.0" } +astroport = "0.3.1" thiserror = { version = "1.0.20" } hex = "0.4" diff --git a/contracts/gov/src/contract.rs b/contracts/gov/src/contract.rs index e3d9621..2abc4c6 100644 --- a/contracts/gov/src/contract.rs +++ b/contracts/gov/src/contract.rs @@ -6,6 +6,7 @@ use crate::state::{ state_store, store_tmp_poll_id, Config, ExecuteData, Poll, State, }; +use astroport::querier::query_token_balance; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -13,7 +14,6 @@ use cosmwasm_std::{ MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; -use terraswap::querier::query_token_balance; use anchor_token::common::OrderBy; use anchor_token::gov::{ diff --git a/contracts/gov/src/staking.rs b/contracts/gov/src/staking.rs index c619842..a59a88d 100644 --- a/contracts/gov/src/staking.rs +++ b/contracts/gov/src/staking.rs @@ -5,12 +5,12 @@ use crate::state::{ }; use anchor_token::gov::{PollStatus, StakerResponse}; +use astroport::querier::query_token_balance; use cosmwasm_std::{ to_binary, Addr, CanonicalAddr, CosmosMsg, Deps, DepsMut, MessageInfo, Response, StdResult, Storage, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; -use terraswap::querier::query_token_balance; pub fn stake_voting_tokens( deps: DepsMut, diff --git a/contracts/gov/src/tests.rs b/contracts/gov/src/tests.rs index e85bfd3..c87a6ca 100644 --- a/contracts/gov/src/tests.rs +++ b/contracts/gov/src/tests.rs @@ -12,13 +12,13 @@ use anchor_token::gov::{ PollStatus, PollsResponse, QueryMsg, StakerResponse, VoteOption, VoterInfo, VotersResponse, VotersResponseItem, }; +use astroport::querier::query_token_balance; use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ attr, coins, from_binary, to_binary, Addr, Api, CanonicalAddr, ContractResult, CosmosMsg, Decimal, Deps, DepsMut, Env, Reply, Response, StdError, SubMsg, Timestamp, Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; -use terraswap::querier::query_token_balance; const VOTING_TOKEN: &str = "voting_token"; const TEST_CREATOR: &str = "creator"; diff --git a/contracts/staking/Cargo.toml b/contracts/staking/Cargo.toml index 18414a1..30aa628 100644 --- a/contracts/staking/Cargo.toml +++ b/contracts/staking/Cargo.toml @@ -37,7 +37,7 @@ backtraces = ["cosmwasm-std/backtraces"] cw20 = { version = "0.8.0" } cosmwasm-std = { version = "0.16.0", features = ["iterator"] } cosmwasm-storage = { version = "0.16.0", features = ["iterator"] } -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/staking/src/contract.rs b/contracts/staking/src/contract.rs index c034c6d..e8f44aa 100644 --- a/contracts/staking/src/contract.rs +++ b/contracts/staking/src/contract.rs @@ -20,6 +20,7 @@ use crate::{ }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; +use std::collections::BTreeMap; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -40,7 +41,7 @@ pub fn instantiate( store_state( deps.storage, &State { - last_distributed: env.block.height, + last_distributed: env.block.time.seconds(), total_bond_amount: Uint128::zero(), global_reward_index: Decimal::zero(), }, @@ -58,6 +59,9 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S ExecuteMsg::MigrateStaking { new_staking_contract, } => migrate_staking(deps, env, info, new_staking_contract), + ExecuteMsg::UpdateConfig { + distribution_schedule, + } => update_config(deps, env, info, distribution_schedule), } } @@ -91,7 +95,7 @@ pub fn bond(deps: DepsMut, env: Env, sender_addr: Addr, amount: Uint128) -> StdR let mut staker_info: StakerInfo = read_staker_info(deps.storage, &sender_addr_raw)?; // Compute global reward & staker reward - compute_reward(&config, &mut state, env.block.height); + compute_reward(&config, &mut state, env.block.time.seconds()); compute_staker_reward(&state, &mut staker_info)?; // Increase bond_amount @@ -120,7 +124,7 @@ pub fn unbond(deps: DepsMut, env: Env, info: MessageInfo, amount: Uint128) -> St } // Compute global reward & staker reward - compute_reward(&config, &mut state, env.block.height); + compute_reward(&config, &mut state, env.block.time.seconds()); compute_staker_reward(&state, &mut staker_info)?; // Decrease bond_amount @@ -162,7 +166,7 @@ pub fn withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult StdResult, +) -> StdResult { + // get gov address by querying anc token minter + let config: Config = read_config(deps.storage)?; + let state: State = read_state(deps.storage)?; + + let sender_addr_raw: CanonicalAddr = deps.api.addr_canonicalize(info.sender.as_str())?; + let anc_token: Addr = deps.api.addr_humanize(&config.anchor_token)?; + let gov_addr_raw: CanonicalAddr = deps + .api + .addr_canonicalize(&query_anc_minter(&deps.querier, anc_token)?)?; + if sender_addr_raw != gov_addr_raw { + return Err(StdError::generic_err("unauthorized")); + } + + assert_new_schedules(&config, &state, distribution_schedule.clone())?; + + let new_config = Config { + anchor_token: config.anchor_token, + staking_token: config.staking_token, + distribution_schedule, + }; + store_config(deps.storage, &new_config)?; + + Ok(Response::new().add_attributes(vec![("action", "update_config")])) +} + pub fn migrate_staking( deps: DepsMut, env: Env, @@ -214,35 +249,35 @@ pub fn migrate_staking( return Err(StdError::generic_err("unauthorized")); } - // compute global reward, sets last_distributed_height to env.block.height - compute_reward(&config, &mut state, env.block.height); + // compute global reward, sets last_distributed_seconds to env.block.time.seconds + compute_reward(&config, &mut state, env.block.time.seconds()); let total_distribution_amount: Uint128 = config.distribution_schedule.iter().map(|item| item.2).sum(); - let block_height = env.block.height; + let block_time = env.block.time.seconds(); // eliminate distribution slots that have not started config .distribution_schedule - .retain(|slot| slot.0 < block_height); + .retain(|slot| slot.0 < block_time); let mut distributed_amount = Uint128::zero(); for s in config.distribution_schedule.iter_mut() { - if s.1 < block_height { + if s.1 < block_time { // all distributed distributed_amount += s.2; } else { // partially distributed slot - let num_blocks = s.1 - s.0; - let distribution_amount_per_block: Decimal = Decimal::from_ratio(s.2, num_blocks); + let whole_time = s.1 - s.0; + let distribution_amount_per_second: Decimal = Decimal::from_ratio(s.2, whole_time); - let passed_blocks = block_height - s.0; + let passed_time = block_time - s.0; let distributed_amount_on_slot = - distribution_amount_per_block * Uint128::from(passed_blocks as u128); + distribution_amount_per_second * Uint128::from(passed_time as u128); distributed_amount += distributed_amount_on_slot; // modify distribution slot - s.1 = block_height; + s.1 = block_time; s.2 = distributed_amount_on_slot; } } @@ -286,28 +321,28 @@ fn decrease_bond_amount( } // compute distributed rewards and update global reward index -fn compute_reward(config: &Config, state: &mut State, block_height: u64) { +fn compute_reward(config: &Config, state: &mut State, block_time: u64) { if state.total_bond_amount.is_zero() { - state.last_distributed = block_height; + state.last_distributed = block_time; return; } let mut distributed_amount: Uint128 = Uint128::zero(); for s in config.distribution_schedule.iter() { - if s.0 > block_height || s.1 < state.last_distributed { + if s.0 > block_time || s.1 < state.last_distributed { continue; } - // min(s.1, block_height) - max(s.0, last_distributed) - let passed_blocks = - std::cmp::min(s.1, block_height) - std::cmp::max(s.0, state.last_distributed); + // min(s.1, block_time) - max(s.0, last_distributed) + let passed_time = + std::cmp::min(s.1, block_time) - std::cmp::max(s.0, state.last_distributed); - let num_blocks = s.1 - s.0; - let distribution_amount_per_block: Decimal = Decimal::from_ratio(s.2, num_blocks); - distributed_amount += distribution_amount_per_block * Uint128::from(passed_blocks as u128); + let time = s.1 - s.0; + let distribution_amount_per_second: Decimal = Decimal::from_ratio(s.2, time); + distributed_amount += distribution_amount_per_second * Uint128::from(passed_time as u128); } - state.last_distributed = block_height; + state.last_distributed = block_time; state.global_reward_index = state.global_reward_index + Decimal::from_ratio(distributed_amount, state.total_bond_amount); } @@ -326,11 +361,10 @@ fn compute_staker_reward(state: &State, staker_info: &mut StakerInfo) -> StdResu pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::Config {} => to_binary(&query_config(deps)?), - QueryMsg::State { block_height } => to_binary(&query_state(deps, block_height)?), - QueryMsg::StakerInfo { - staker, - block_height, - } => to_binary(&query_staker_info(deps, staker, block_height)?), + QueryMsg::State { block_time } => to_binary(&query_state(deps, block_time)?), + QueryMsg::StakerInfo { staker, block_time } => { + to_binary(&query_staker_info(deps, staker, block_time)?) + } } } @@ -345,11 +379,11 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(resp) } -pub fn query_state(deps: Deps, block_height: Option) -> StdResult { +pub fn query_state(deps: Deps, block_time: Option) -> StdResult { let mut state: State = read_state(deps.storage)?; - if let Some(block_height) = block_height { + if let Some(block_time) = block_time { let config = read_config(deps.storage)?; - compute_reward(&config, &mut state, block_height); + compute_reward(&config, &mut state, block_time); } Ok(StateResponse { @@ -362,16 +396,16 @@ pub fn query_state(deps: Deps, block_height: Option) -> StdResult, + block_time: Option, ) -> StdResult { let staker_raw = deps.api.addr_canonicalize(&staker)?; let mut staker_info: StakerInfo = read_staker_info(deps.storage, &staker_raw)?; - if let Some(block_height) = block_height { + if let Some(block_time) = block_time { let config = read_config(deps.storage)?; let mut state = read_state(deps.storage)?; - compute_reward(&config, &mut state, block_height); + compute_reward(&config, &mut state, block_time); compute_staker_reward(&state, &mut staker_info)?; } @@ -383,6 +417,52 @@ pub fn query_staker_info( }) } +pub fn assert_new_schedules( + config: &Config, + state: &State, + distribution_schedule: Vec<(u64, u64, Uint128)>, +) -> StdResult<()> { + if distribution_schedule.len() < config.distribution_schedule.len() { + return Err(StdError::generic_err( + "cannot update; the new schedule must support all of the previous schedule", + )); + } + + let mut existing_counts: BTreeMap<(u64, u64, Uint128), u32> = BTreeMap::new(); + for schedule in config.distribution_schedule.clone() { + let counter = existing_counts.entry(schedule).or_insert(0); + *counter += 1; + } + + let mut new_counts: BTreeMap<(u64, u64, Uint128), u32> = BTreeMap::new(); + for schedule in distribution_schedule.clone() { + let counter = new_counts.entry(schedule).or_insert(0); + *counter += 1; + } + + for (schedule, count) in existing_counts.into_iter() { + // if began ensure its in the new schedule + if schedule.0 <= state.last_distributed { + if count > *new_counts.get(&schedule).unwrap_or(&0u32) { + return Err(StdError::generic_err( + "new schedule removes already started distribution", + )); + } + // after this new_counts will only contain the newly added schedules + *new_counts.get_mut(&schedule).unwrap() -= count; + } + } + + for (schedule, count) in new_counts.into_iter() { + if count > 0 && schedule.0 <= state.last_distributed { + return Err(StdError::generic_err( + "new schedule adds an already started distribution", + )); + } + } + Ok(()) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { Ok(Response::default()) diff --git a/contracts/staking/src/testing.rs b/contracts/staking/src/testing.rs index 51391e9..f75bdaf 100644 --- a/contracts/staking/src/testing.rs +++ b/contracts/staking/src/testing.rs @@ -1,5 +1,6 @@ use crate::contract::{execute, instantiate, query}; use crate::mock_querier::mock_dependencies; +use anchor_token::staking::ExecuteMsg::UpdateConfig; use anchor_token::staking::{ ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StakerInfoResponse, StateResponse, @@ -40,14 +41,14 @@ fn proper_initialization() { let res = query( deps.as_ref(), mock_env(), - QueryMsg::State { block_height: None }, + QueryMsg::State { block_time: None }, ) .unwrap(); let state: StateResponse = from_binary(&res).unwrap(); assert_eq!( state, StateResponse { - last_distributed: 12345, + last_distributed: mock_env().block.time.seconds(), total_bond_amount: Uint128::zero(), global_reward_index: Decimal::zero(), } @@ -62,8 +63,16 @@ fn test_bond_tokens() { anchor_token: "reward0000".to_string(), staking_token: "staking0000".to_string(), distribution_schedule: vec![ - (12345, 12345 + 100, Uint128::from(1000000u128)), - (12345 + 100, 12345 + 200, Uint128::from(10000000u128)), + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), ], }; @@ -87,7 +96,7 @@ fn test_bond_tokens() { mock_env(), QueryMsg::StakerInfo { staker: "addr0000".to_string(), - block_height: None, + block_time: None, }, ) .unwrap(), @@ -106,7 +115,7 @@ fn test_bond_tokens() { &query( deps.as_ref(), mock_env(), - QueryMsg::State { block_height: None } + QueryMsg::State { block_time: None } ) .unwrap() ) @@ -114,7 +123,7 @@ fn test_bond_tokens() { StateResponse { total_bond_amount: Uint128::from(100u128), global_reward_index: Decimal::zero(), - last_distributed: 12345, + last_distributed: mock_env().block.time.seconds(), } ); @@ -124,7 +133,7 @@ fn test_bond_tokens() { amount: Uint128::from(100u128), msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), }); - env.block.height += 10; + env.block.time = env.block.time.plus_seconds(10); let _res = execute(deps.as_mut(), env, info, msg).unwrap(); @@ -135,7 +144,7 @@ fn test_bond_tokens() { mock_env(), QueryMsg::StakerInfo { staker: "addr0000".to_string(), - block_height: None, + block_time: None, }, ) .unwrap(), @@ -154,7 +163,7 @@ fn test_bond_tokens() { &query( deps.as_ref(), mock_env(), - QueryMsg::State { block_height: None } + QueryMsg::State { block_time: None } ) .unwrap() ) @@ -162,7 +171,7 @@ fn test_bond_tokens() { StateResponse { total_bond_amount: Uint128::from(200u128), global_reward_index: Decimal::from_ratio(1000u128, 1u128), - last_distributed: 12345 + 10, + last_distributed: mock_env().block.time.seconds() + 10, } ); @@ -249,8 +258,16 @@ fn test_compute_reward() { anchor_token: "reward0000".to_string(), staking_token: "staking0000".to_string(), distribution_schedule: vec![ - (12345, 12345 + 100, Uint128::from(1000000u128)), - (12345 + 100, 12345 + 200, Uint128::from(10000000u128)), + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), ], }; @@ -267,9 +284,9 @@ fn test_compute_reward() { let mut env = mock_env(); let _res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - // 100 blocks passed + // 100 seconds passed // 1,000,000 rewards distributed - env.block.height += 100; + env.block.time = env.block.time.plus_seconds(100); // bond 100 more tokens let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { @@ -286,7 +303,7 @@ fn test_compute_reward() { mock_env(), QueryMsg::StakerInfo { staker: "addr0000".to_string(), - block_height: None, + block_time: None, }, ) .unwrap() @@ -300,9 +317,9 @@ fn test_compute_reward() { } ); - // 100 blocks passed + // 100 seconds passed // 1,000,000 rewards distributed - env.block.height += 10; + env.block.time = env.block.time.plus_seconds(10); let info = mock_info("addr0000", &[]); // unbond @@ -317,7 +334,7 @@ fn test_compute_reward() { mock_env(), QueryMsg::StakerInfo { staker: "addr0000".to_string(), - block_height: None, + block_time: None, }, ) .unwrap() @@ -339,7 +356,7 @@ fn test_compute_reward() { mock_env(), QueryMsg::StakerInfo { staker: "addr0000".to_string(), - block_height: Some(12345 + 120), + block_time: Some(mock_env().block.time.plus_seconds(120).seconds()), }, ) .unwrap() @@ -362,8 +379,16 @@ fn test_withdraw() { anchor_token: "reward0000".to_string(), staking_token: "staking0000".to_string(), distribution_schedule: vec![ - (12345, 12345 + 100, Uint128::from(1000000u128)), - (12345 + 100, 12345 + 200, Uint128::from(10000000u128)), + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), ], }; @@ -380,9 +405,10 @@ fn test_withdraw() { let mut env = mock_env(); let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - // 100 blocks passed + // 100 seconds passed // 1,000,000 rewards distributed - env.block.height += 100; + env.block.time = env.block.time.plus_seconds(100); + let info = mock_info("addr0000", &[]); let msg = ExecuteMsg::Withdraw {}; @@ -410,8 +436,16 @@ fn test_migrate_staking() { anchor_token: "reward0000".to_string(), staking_token: "staking0000".to_string(), distribution_schedule: vec![ - (12345, 12345 + 100, Uint128::from(1000000u128)), - (12345 + 100, 12345 + 200, Uint128::from(10000000u128)), + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), ], }; @@ -428,9 +462,9 @@ fn test_migrate_staking() { let mut env = mock_env(); let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - // 100 blocks passed + // 100 seconds is passed // 1,000,000 rewards distributed - env.block.height += 100; + env.block.time = env.block.time.plus_seconds(100); let info = mock_info("addr0000", &[]); let msg = ExecuteMsg::Withdraw {}; @@ -449,8 +483,8 @@ fn test_migrate_staking() { }))] ); - // execute migration after 50 blocks - env.block.height += 50; + // execute migration after 50 seconds + env.block.time = env.block.time.plus_seconds(50); deps.querier.with_anc_minter("gov0000".to_string()); @@ -501,9 +535,503 @@ fn test_migrate_staking() { anchor_token: "reward0000".to_string(), staking_token: "staking0000".to_string(), distribution_schedule: vec![ - (12345, 12345 + 100, Uint128::from(1000000u128)), - (12345 + 100, 12345 + 150, Uint128::from(5000000u128)), // slot was modified + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128) + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 150, + Uint128::from(5000000u128) + ), // slot was modified ] } ); } + +#[test] +fn test_update_config() { + let mut deps = mock_dependencies(&[]); + + let msg = InstantiateMsg { + anchor_token: "reward0000".to_string(), + staking_token: "staking0000".to_string(), + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(10000000u128), + ), + ], + }; + + let info = mock_info("addr0000", &[]); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + let update_config = UpdateConfig { + distribution_schedule: vec![( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(10000000u128), + )], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("notgov", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config); + match res { + Err(StdError::GenericErr { msg, .. }) => assert_eq!(msg, "unauthorized"), + _ => panic!("Must return unauthorized error"), + } + + // do some bond and update rewards + // bond 100 tokens + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: "addr0000".to_string(), + amount: Uint128::from(100u128), + msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), + }); + let info = mock_info("staking0000", &[]); + let mut env = mock_env(); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // 100 seconds is passed + // 1,000,000 rewards distributed + env.block.time = env.block.time.plus_seconds(100); + let info = mock_info("addr0000", &[]); + + let msg = ExecuteMsg::Withdraw {}; + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!( + res.messages, + vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "reward0000".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: "addr0000".to_string(), + amount: Uint128::from(1000000u128), + }) + .unwrap(), + funds: vec![], + }))] + ); + + let update_config = UpdateConfig { + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(5000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(10000000u128), + ), + ], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("gov0000", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config); + match res { + Err(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, "new schedule removes already started distribution") + } + _ => panic!("Must return unauthorized error"), + } + + // do some bond and update rewards + // bond 100 tokens + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: "addr0000".to_string(), + amount: Uint128::from(100u128), + msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), + }); + let info = mock_info("staking0000", &[]); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // 100 seconds is passed + // 1,000,000 rewards distributed + env.block.time = env.block.time.plus_seconds(100); + + let info = mock_info("addr0000", &[]); + + let msg = ExecuteMsg::Withdraw {}; + let _res = execute(deps.as_mut(), env, info, msg).unwrap(); + + //cannot update previous scehdule + let update_config = UpdateConfig { + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(5000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(10000000u128), + ), + ], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("gov0000", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config); + match res { + Err(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, "new schedule removes already started distribution") + } + _ => panic!("Must return unauthorized error"), + } + + //successful one + let update_config = UpdateConfig { + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(20000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(10000000u128), + ), + ], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("gov0000", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config).unwrap(); + + assert_eq!(res.attributes, vec![("action", "update_config")]); + + // query config + let res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let config: ConfigResponse = from_binary(&res).unwrap(); + assert_eq!( + config.distribution_schedule, + vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(20000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(10000000u128), + ), + ] + ); + + //successful one + let update_config = UpdateConfig { + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(20000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(50000000u128), + ), + ], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("gov0000", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config).unwrap(); + + assert_eq!(res.attributes, vec![("action", "update_config")]); + + // query config + let res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let config: ConfigResponse = from_binary(&res).unwrap(); + assert_eq!( + config.distribution_schedule, + vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(20000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(50000000u128), + ), + ] + ); + + let update_config = UpdateConfig { + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(90000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(80000000u128), + ), + ], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("gov0000", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config).unwrap(); + + assert_eq!(res.attributes, vec![("action", "update_config")]); + + // query config + let res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let config: ConfigResponse = from_binary(&res).unwrap(); + assert_eq!( + config.distribution_schedule, + vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(90000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(80000000u128), + ), + ] + ); + + let update_config = UpdateConfig { + distribution_schedule: vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(90000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(80000000u128), + ), + ( + mock_env().block.time.seconds() + 500, + mock_env().block.time.seconds() + 600, + Uint128::from(60000000u128), + ), + ], + }; + + deps.querier.with_anc_minter("gov0000".to_string()); + + let info = mock_info("gov0000", &[]); + let res = execute(deps.as_mut(), mock_env(), info, update_config).unwrap(); + + assert_eq!(res.attributes, vec![("action", "update_config")]); + + // query config + let res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let config: ConfigResponse = from_binary(&res).unwrap(); + assert_eq!( + config.distribution_schedule, + vec![ + ( + mock_env().block.time.seconds(), + mock_env().block.time.seconds() + 100, + Uint128::from(1000000u128), + ), + ( + mock_env().block.time.seconds() + 100, + mock_env().block.time.seconds() + 200, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 200, + mock_env().block.time.seconds() + 300, + Uint128::from(10000000u128), + ), + ( + mock_env().block.time.seconds() + 300, + mock_env().block.time.seconds() + 400, + Uint128::from(90000000u128), + ), + ( + mock_env().block.time.seconds() + 400, + mock_env().block.time.seconds() + 500, + Uint128::from(80000000u128), + ), + ( + mock_env().block.time.seconds() + 500, + mock_env().block.time.seconds() + 600, + Uint128::from(60000000u128), + ) + ] + ); +} diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 972c508..cc0f297 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -29,7 +29,7 @@ cosmwasm-std = { version = "0.16.0", features = ["iterator"] } cosmwasm-storage = { version = "0.16.0", features = ["iterator"] } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -anchor-token = { version = "0.2.0", path = "../../packages/anchor_token" } +anchor-token = { version = "0.3.0", path = "../../packages/anchor_token" } [dev-dependencies] cosmwasm-schema = { version = "0.16.0", default-features = false } diff --git a/packages/anchor_token/Cargo.toml b/packages/anchor_token/Cargo.toml index 42702ab..e0bf0cd 100644 --- a/packages/anchor_token/Cargo.toml +++ b/packages/anchor_token/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "anchor-token" -version = "0.2.0" +version = "0.3.0" authors = ["Terraform Labs, PTE."] edition = "2018" description = "Common helpers for other anchor-token specs" @@ -21,7 +21,7 @@ cw20 = { version = "0.8.0" } cosmwasm-bignumber = "2.2.0" cosmwasm-std = { version = "0.16.0" } cosmwasm-storage = { version = "0.16.0" } -terra-cosmwasm = "2.2.0" +terra-cosmwasm = "2.2.0" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/anchor_token/src/collector.rs b/packages/anchor_token/src/collector.rs index aa6e89e..5ee85ce 100644 --- a/packages/anchor_token/src/collector.rs +++ b/packages/anchor_token/src/collector.rs @@ -6,10 +6,10 @@ use cosmwasm_std::Decimal; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InstantiateMsg { pub gov_contract: String, // collected rewards receiver - pub terraswap_factory: String, + pub astroport_factory: String, pub anchor_token: String, - pub distributor_contract: String, pub reward_factor: Decimal, + pub max_spread: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -17,7 +17,17 @@ pub struct InstantiateMsg { pub enum ExecuteMsg { /// Update config interface /// to enable reward_factor update - UpdateConfig { reward_factor: Option }, + /// ## NOTE: + /// for updating `max spread` + /// it should be either (true, none) or (true, "0.1") + /// if we do not want to update it + /// it should be (false, none) + UpdateConfig { + reward_factor: Option, + gov_contract: Option, + astroport_factory: Option, + max_spread: (bool, Option), + }, /// Public Message /// Sweep all given denom balance to ANC token /// and execute Distribute message @@ -34,12 +44,15 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct ConfigResponse { pub gov_contract: String, // collected rewards receiver - pub terraswap_factory: String, + pub astroport_factory: String, pub anchor_token: String, - pub distributor_contract: String, pub reward_factor: Decimal, + pub max_spread: Option, } /// We currently take no arguments for migrations #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct MigrateMsg {} +pub struct MigrateMsg { + pub astroport_factory: String, + pub max_spread: Decimal, +} diff --git a/packages/anchor_token/src/staking.rs b/packages/anchor_token/src/staking.rs index 2d7254c..f3089f5 100644 --- a/packages/anchor_token/src/staking.rs +++ b/packages/anchor_token/src/staking.rs @@ -25,6 +25,9 @@ pub enum ExecuteMsg { MigrateStaking { new_staking_contract: String, }, + UpdateConfig { + distribution_schedule: Vec<(u64, u64, Uint128)>, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -33,7 +36,8 @@ pub enum Cw20HookMsg { Bond {}, } -/// We currently take no arguments for migrations +/// migrate struct for distribution schedule +/// block-based schedule to a time-based schedule #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct MigrateMsg {} @@ -42,11 +46,11 @@ pub struct MigrateMsg {} pub enum QueryMsg { Config {}, State { - block_height: Option, + block_time: Option, }, StakerInfo { staker: String, - block_height: Option, + block_time: Option, }, }