diff --git a/Cargo.lock b/Cargo.lock index ce2082c52..2309a7203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1726,43 +1726,44 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "mars-address-provider" -version = "1.2.0" +version = "2.0.0" dependencies = [ "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "serde", "thiserror", ] [[package]] name = "mars-health" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "mars-params", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-testing", "thiserror", ] [[package]] name = "mars-incentives" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", + "mars-owner 2.0.0", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 1.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "test-case", "thiserror", @@ -1770,7 +1771,7 @@ dependencies = [ [[package]] name = "mars-integration-tests" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-std", @@ -1782,11 +1783,11 @@ dependencies = [ "mars-osmosis", "mars-params", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-osmosis", "mars-swapper-osmosis", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "osmosis-test-tube", "serde", @@ -1794,12 +1795,12 @@ dependencies = [ [[package]] name = "mars-interest-rate" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "mars-red-bank-types", - "mars-utils", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", ] [[package]] @@ -1814,7 +1815,7 @@ dependencies = [ [[package]] name = "mars-mock-pyth" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1823,14 +1824,14 @@ dependencies = [ [[package]] name = "mars-oracle-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types", - "mars-utils", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", "pyth-sdk-cw", "schemars", "serde", @@ -1839,7 +1840,7 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1847,10 +1848,10 @@ dependencies = [ "cw2 1.1.0", "mars-oracle-base", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "pyth-sdk-cw", "schemars", @@ -1859,7 +1860,7 @@ dependencies = [ [[package]] name = "mars-oracle-wasm" -version = "1.2.0" +version = "2.0.0" dependencies = [ "astroport", "cosmwasm-schema", @@ -1868,8 +1869,8 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-oracle-base", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", "proptest", "pyth-sdk-cw", @@ -1878,7 +1879,7 @@ dependencies = [ [[package]] name = "mars-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "osmosis-std", @@ -1886,6 +1887,19 @@ dependencies = [ "serde", ] +[[package]] +name = "mars-owner" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd53908ffc561da878ce5ff4f5ec9f25a193af28ec0b6e7c8e6d1a0866d9dfc" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "schemars", + "thiserror", +] + [[package]] name = "mars-owner" version = "2.0.0" @@ -1901,7 +1915,7 @@ dependencies = [ [[package]] name = "mars-params" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1910,17 +1924,17 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-interest-rate", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "test-case", "thiserror", ] [[package]] name = "mars-red-bank" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1932,11 +1946,11 @@ dependencies = [ "mars-health", "mars-interest-rate", "mars-liquidation", - "mars-owner", + "mars-owner 2.0.0", "mars-params", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "pyth-sdk-cw", "test-case", "thiserror", @@ -1944,26 +1958,38 @@ dependencies = [ [[package]] name = "mars-red-bank-types" -version = "1.2.0" +version = "1.0.0" +source = "git+https://github.com/mars-protocol/red-bank?tag=v1.0.0#13fcc446fe687dfb8f08ffacd57b1ab6ad6cfcc9" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "mars-owner 1.2.0", + "mars-utils 1.0.0", + "thiserror", +] + +[[package]] +name = "mars-red-bank-types" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "mars-owner", - "mars-utils", + "mars-owner 2.0.0", + "mars-utils 2.0.0", "strum 0.25.0", "thiserror", ] [[package]] name = "mars-rewards-collector-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "mars-owner", - "mars-red-bank-types", - "mars-utils", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", "schemars", "serde", "thiserror", @@ -1971,34 +1997,34 @@ dependencies = [ [[package]] name = "mars-rewards-collector-neutron" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw2 1.1.0", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-base", "neutron-sdk", ] [[package]] name = "mars-rewards-collector-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw2 1.1.0", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-base", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "serde", ] [[package]] name = "mars-swapper-astroport" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "astroport", @@ -2007,7 +2033,7 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-oracle-wasm", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-swapper-base", "mars-testing", "test-case", @@ -2015,14 +2041,14 @@ dependencies = [ [[package]] name = "mars-swapper-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-paginate", "cw-storage-plus 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "schemars", "serde", "thiserror", @@ -2030,15 +2056,15 @@ dependencies = [ [[package]] name = "mars-swapper-mock" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", ] [[package]] name = "mars-swapper-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2046,15 +2072,15 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-swapper-base", "osmosis-std", ] [[package]] name = "mars-testing" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "astroport", @@ -2068,10 +2094,10 @@ dependencies = [ "mars-oracle-osmosis", "mars-oracle-wasm", "mars-osmosis", - "mars-owner", + "mars-owner 2.0.0", "mars-params", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-osmosis", "mars-swapper-astroport", "osmosis-std", @@ -2081,7 +2107,16 @@ dependencies = [ [[package]] name = "mars-utils" -version = "1.2.0" +version = "1.0.0" +source = "git+https://github.com/mars-protocol/red-bank?tag=v1.0.0#13fcc446fe687dfb8f08ffacd57b1ab6ad6cfcc9" +dependencies = [ + "cosmwasm-std", + "thiserror", +] + +[[package]] +name = "mars-utils" +version = "2.0.0" dependencies = [ "cosmwasm-std", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 71a9d92e2..814aeb606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.2.0" +version = "2.0.0" authors = [ "Gabe R. ", "Larry Engineer ", @@ -65,13 +65,13 @@ proptest = "1.2.0" test-case = "3.1.0" # packages -mars-health = { path = "./packages/health" } -mars-interest-rate = { path = "./packages/interest-rate" } -mars-liquidation = { path = "./packages/liquidation" } -mars-osmosis = { path = "./packages/chains/osmosis" } -mars-red-bank-types = { path = "./packages/types" } -mars-testing = { path = "./packages/testing" } -mars-utils = { path = "./packages/utils" } +mars-health = { path = "./packages/health" } +mars-interest-rate = { path = "./packages/interest-rate" } +mars-liquidation = { path = "./packages/liquidation" } +mars-osmosis = { path = "./packages/chains/osmosis" } +mars-red-bank-types = { path = "./packages/types" } +mars-testing = { path = "./packages/testing" } +mars-utils = { path = "./packages/utils" } # contracts mars-address-provider = { path = "./contracts/address-provider" } diff --git a/contracts/incentives/Cargo.toml b/contracts/incentives/Cargo.toml index 68d95a662..37ec5391e 100644 --- a/contracts/incentives/Cargo.toml +++ b/contracts/incentives/Cargo.toml @@ -19,16 +19,19 @@ doctest = false backtraces = ["cosmwasm-std/backtraces", "mars-testing/backtraces", "mars-utils/backtraces", "mars-red-bank/backtraces"] [dependencies] -cosmwasm-std = { workspace = true } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-utils = { workspace = true } -thiserror = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +mars-owner = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-utils = { workspace = true } +thiserror = { workspace = true } + +# Old red-bank types used for migration. +mars-red-bank-types-old = { package = "mars-red-bank-types", git = "https://github.com/mars-protocol/red-bank", tag = "v1.0.0" } [dev-dependencies] -cosmwasm-schema = { workspace = true } mars-testing = { workspace = true } osmosis-std = { workspace = true } mars-red-bank = { workspace = true } diff --git a/contracts/incentives/src/contract.rs b/contracts/incentives/src/contract.rs index 22f3d099e..849a0055b 100644 --- a/contracts/incentives/src/contract.rs +++ b/contracts/incentives/src/contract.rs @@ -22,6 +22,7 @@ use crate::{ helpers::{ self, compute_user_accrued_rewards, compute_user_unclaimed_rewards, update_incentive_index, }, + migrations, state::{ self, CONFIG, DEFAULT_LIMIT, EMISSIONS, EPOCH_DURATION, INCENTIVE_STATES, MAX_LIMIT, OWNER, USER_ASSET_INDICES, USER_UNCLAIMED_REWARDS, WHITELIST, WHITELIST_COUNT, @@ -765,6 +766,8 @@ pub fn query_emissions( /// MIGRATION #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::default()) +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + match msg { + MigrateMsg::V1_0_0ToV2_0_0(updates) => migrations::v2_0_0::migrate(deps, env, updates), + } } diff --git a/contracts/incentives/src/error.rs b/contracts/incentives/src/error.rs index 7a77a92f8..167efdfb0 100644 --- a/contracts/incentives/src/error.rs +++ b/contracts/incentives/src/error.rs @@ -66,6 +66,9 @@ pub enum ContractError { DuplicateDenom { denom: String, }, + + #[error("{0}")] + Version(#[from] cw2::VersionError), } impl From for StdError { diff --git a/contracts/incentives/src/lib.rs b/contracts/incentives/src/lib.rs index 627a6e152..88d83aeaf 100644 --- a/contracts/incentives/src/lib.rs +++ b/contracts/incentives/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; mod error; pub mod helpers; +pub mod migrations; pub mod state; pub use error::ContractError; diff --git a/contracts/incentives/src/migrations/mod.rs b/contracts/incentives/src/migrations/mod.rs new file mode 100644 index 000000000..7592b6f12 --- /dev/null +++ b/contracts/incentives/src/migrations/mod.rs @@ -0,0 +1 @@ +pub mod v2_0_0; diff --git a/contracts/incentives/src/migrations/v2_0_0.rs b/contracts/incentives/src/migrations/v2_0_0.rs new file mode 100644 index 000000000..4e3da098a --- /dev/null +++ b/contracts/incentives/src/migrations/v2_0_0.rs @@ -0,0 +1,286 @@ +use std::collections::HashMap; + +use cosmwasm_std::{DepsMut, Env, Order, Response, StdResult, Uint128}; +use cw2::{assert_contract_version, set_contract_version}; +use mars_owner::OwnerInit; +use mars_red_bank_types::incentives::{Config, IncentiveState, V2Updates}; + +use crate::{ + contract::{CONTRACT_NAME, CONTRACT_VERSION, MIN_EPOCH_DURATION}, + error::ContractError, + state::{ + CONFIG, EPOCH_DURATION, INCENTIVE_STATES, OWNER, USER_ASSET_INDICES, + USER_UNCLAIMED_REWARDS, WHITELIST, WHITELIST_COUNT, + }, +}; + +const FROM_VERSION: &str = "1.0.0"; + +pub mod v1_state { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Addr, Decimal, Uint128}; + use cw_storage_plus::{Item, Map}; + use mars_red_bank_types_old::incentives::{AssetIncentive, Config}; + + pub const OWNER: Item = Item::new("owner"); + pub const CONFIG: Item = Item::new("config"); + + pub const ASSET_INCENTIVES: Map<&str, AssetIncentive> = Map::new("incentives"); + pub const USER_ASSET_INDICES: Map<(&Addr, &str), Decimal> = Map::new("indices"); + pub const USER_UNCLAIMED_REWARDS: Map<&Addr, Uint128> = Map::new("unclaimed_rewards"); + + #[cw_serde] + pub enum OwnerState { + B(OwnerSetNoneProposed), + } + + #[cw_serde] + pub struct OwnerSetNoneProposed { + pub owner: Addr, + } + + pub fn current_owner(state: OwnerState) -> Addr { + match state { + OwnerState::B(b) => b.owner, + } + } + + // Copy of helpers from v1.0.0 tag: + // https://github.com/mars-protocol/red-bank/blob/v1.0.0/contracts/incentives/src/helpers.rs + // Included as dependency coudn't generate proper schema for mars-incentive, even with specified + // version. + pub mod helpers { + use std::cmp::{max, min}; + + use cosmwasm_std::{ + Decimal, OverflowError, OverflowOperation, StdError, StdResult, Uint128, + }; + use mars_red_bank_types_old::incentives::AssetIncentive; + + /// Updates asset incentive index and last updated timestamp by computing + /// how many rewards were accrued since last time updated given incentive's + /// emission per second. + /// Total supply is the total (liquidity) token supply during the period being computed. + /// Note that this method does not commit updates to state as that should be executed by the + /// caller + pub fn update_asset_incentive_index( + asset_incentive: &mut AssetIncentive, + total_amount_scaled: Uint128, + current_block_time: u64, + ) -> StdResult<()> { + let end_time_sec = asset_incentive.start_time + asset_incentive.duration; + if (current_block_time != asset_incentive.last_updated) + && current_block_time > asset_incentive.start_time + && asset_incentive.last_updated < end_time_sec + && !total_amount_scaled.is_zero() + && !asset_incentive.emission_per_second.is_zero() + { + let time_start = max(asset_incentive.start_time, asset_incentive.last_updated); + let time_end = min(current_block_time, end_time_sec); + asset_incentive.index = compute_asset_incentive_index( + asset_incentive.index, + asset_incentive.emission_per_second, + total_amount_scaled, + time_start, + time_end, + )?; + } + asset_incentive.last_updated = current_block_time; + Ok(()) + } + + pub fn compute_asset_incentive_index( + previous_index: Decimal, + emission_per_second: Uint128, + total_amount_scaled: Uint128, + time_start: u64, + time_end: u64, + ) -> StdResult { + if time_start > time_end { + return Err(StdError::overflow(OverflowError::new( + OverflowOperation::Sub, + time_start, + time_end, + ))); + } + let seconds_elapsed = time_end - time_start; + let emission_for_elapsed_seconds = + emission_per_second.checked_mul(Uint128::from(seconds_elapsed))?; + let new_index = previous_index + + Decimal::from_ratio(emission_for_elapsed_seconds, total_amount_scaled); + Ok(new_index) + } + + /// Computes user accrued rewards using the difference between asset_incentive index and + /// user current index + /// asset_incentives index should be up to date. + pub fn compute_user_accrued_rewards( + user_amount_scaled: Uint128, + user_asset_index: Decimal, + asset_incentive_index: Decimal, + ) -> StdResult { + let result = (user_amount_scaled * asset_incentive_index) + .checked_sub(user_amount_scaled * user_asset_index)?; + Ok(result) + } + } +} + +pub fn migrate(mut deps: DepsMut, env: Env, updates: V2Updates) -> Result { + // make sure we're migrating the correct contract and from the correct version + assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; + + // Owner package updated, re-initializing + let old_owner_state = v1_state::OWNER.load(deps.storage)?; + let old_owner = v1_state::current_owner(old_owner_state); + v1_state::OWNER.remove(deps.storage); + OWNER.initialize( + deps.storage, + deps.api, + OwnerInit::SetInitialOwner { + owner: old_owner.to_string(), + }, + )?; + + // CONFIG updated, re-initializing + let old_config_state = v1_state::CONFIG.load(deps.storage)?; + v1_state::CONFIG.remove(deps.storage); + CONFIG.save( + deps.storage, + &Config { + address_provider: old_config_state.address_provider, + max_whitelisted_denoms: updates.max_whitelisted_denoms, + }, + )?; + + // WHITELIST not existent in v1, initializing + WHITELIST.save(deps.storage, &old_config_state.mars_denom, &Uint128::one())?; + WHITELIST_COUNT.save(deps.storage, &1)?; + + // EPOCH_DURATION not existent in v1, initializing + if updates.epoch_duration < MIN_EPOCH_DURATION { + return Err(ContractError::EpochDurationTooShort { + min_epoch_duration: MIN_EPOCH_DURATION, + }); + } + EPOCH_DURATION.save(deps.storage, &updates.epoch_duration)?; + + migrate_indices_and_unclaimed_rewards(&mut deps, env, &old_config_state.mars_denom)?; + + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute("from_version", FROM_VERSION) + .add_attribute("to_version", CONTRACT_VERSION)) +} + +// Migrate indices and unclaimed rewards from v1 to v2 with helpers from v1.0.0 tag: +// https://github.com/mars-protocol/red-bank/blob/v1.0.0/contracts/incentives/src/helpers.rs +// +// This is done by querying the Red Bank contract for the collateral total supply and +// user collateral amount for each collateral denom. +fn migrate_indices_and_unclaimed_rewards( + deps: &mut DepsMut, + env: Env, + mars_denom: &str, +) -> Result<(), ContractError> { + let current_block_time = env.block.time.seconds(); + + let config = CONFIG.load(deps.storage)?; + + let red_bank_addr = mars_red_bank_types::address_provider::helpers::query_contract_addr( + deps.as_ref(), + &config.address_provider, + mars_red_bank_types::address_provider::MarsAddressType::RedBank, + )?; + + let mut asset_incentives = v1_state::ASSET_INCENTIVES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::ASSET_INCENTIVES.clear(deps.storage); + + for (denom, asset_incentive) in asset_incentives.iter_mut() { + let market: mars_red_bank_types::red_bank::Market = deps.querier.query_wasm_smart( + red_bank_addr.clone(), + &mars_red_bank_types::red_bank::QueryMsg::Market { + denom: denom.clone(), + }, + )?; + + v1_state::helpers::update_asset_incentive_index( + asset_incentive, + market.collateral_total_scaled, + current_block_time, + )?; + + // Update incentive state for collateral and incentive denom (Mars) + INCENTIVE_STATES.save( + deps.storage, + (denom, mars_denom), + &IncentiveState { + index: asset_incentive.index, + last_updated: current_block_time, + }, + )?; + } + + let user_asset_indices = v1_state::USER_ASSET_INDICES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::USER_ASSET_INDICES.clear(deps.storage); + + let mut user_unclaimed_rewards = v1_state::USER_UNCLAIMED_REWARDS + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::USER_UNCLAIMED_REWARDS.clear(deps.storage); + + for ((user, denom), user_asset_index) in user_asset_indices { + let collateral: mars_red_bank_types::red_bank::UserCollateralResponse = + deps.querier.query_wasm_smart( + red_bank_addr.clone(), + &mars_red_bank_types::red_bank::QueryMsg::UserCollateral { + user: user.to_string(), + account_id: None, + denom: denom.clone(), + }, + )?; + + // Get asset incentive for a denom. It should be available but just in case we don't unwrap + let denom_idx = asset_incentives.get(&denom); + if let Some(asset_incentive) = denom_idx { + // Since we didn't track unclaimed rewards per collateral denom in v1 we add them + // to the user unclaimed rewards for the first user collateral denom. + let mut unclaimed_rewards = user_unclaimed_rewards.remove(&user).unwrap_or_default(); + + if user_asset_index != asset_incentive.index { + // Compute user accrued rewards + let asset_accrued_rewards = v1_state::helpers::compute_user_accrued_rewards( + collateral.amount_scaled, + user_asset_index, + asset_incentive.index, + )?; + + unclaimed_rewards += asset_accrued_rewards; + } + + if !unclaimed_rewards.is_zero() { + // Update user unclaimed rewards + USER_UNCLAIMED_REWARDS.save( + deps.storage, + ((&user, ""), &denom, mars_denom), + &unclaimed_rewards, + )?; + } + + // Update user asset index + USER_ASSET_INDICES.save( + deps.storage, + ((&user, ""), &denom, mars_denom), + &asset_incentive.index, + )?; + } + } + + Ok(()) +} diff --git a/contracts/incentives/tests/tests/mod.rs b/contracts/incentives/tests/tests/mod.rs index 85b241bca..e95d9f695 100644 --- a/contracts/incentives/tests/tests/mod.rs +++ b/contracts/incentives/tests/tests/mod.rs @@ -4,6 +4,7 @@ mod test_admin; mod test_balance_change; mod test_claim_rewards; mod test_indices_usage; +mod test_migration_v2; mod test_quering; mod test_set_asset_incentive; mod test_update_owner; diff --git a/contracts/incentives/tests/tests/test_migration_v2.rs b/contracts/incentives/tests/tests/test_migration_v2.rs new file mode 100644 index 000000000..463ca2529 --- /dev/null +++ b/contracts/incentives/tests/tests/test_migration_v2.rs @@ -0,0 +1,401 @@ +use std::collections::HashMap; + +use cosmwasm_std::{ + attr, testing::mock_env, Addr, Decimal, Event, Order, StdResult, Timestamp, Uint128, +}; +use cw2::VersionError; +use mars_incentives::{ + contract::migrate, + migrations::v2_0_0::v1_state::{self, OwnerSetNoneProposed}, + state::{ + CONFIG, INCENTIVE_STATES, OWNER, USER_ASSET_INDICES, USER_UNCLAIMED_REWARDS, WHITELIST, + WHITELIST_COUNT, + }, + ContractError, +}; +use mars_red_bank_types::{ + incentives::{Config, IncentiveState, MigrateMsg, V2Updates}, + red_bank::{Market, UserCollateralResponse}, +}; +use mars_testing::{mock_dependencies, MockEnvParams}; + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "1.0.0").unwrap(); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }), + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: "crates.io:mars-incentives".to_string(), + found: "contract_xyz".to_string() + }) + ); +} + +#[test] +fn wrong_contract_version() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-incentives", "4.1.0").unwrap(); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }), + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: "1.0.0".to_string(), + found: "4.1.0".to_string() + }) + ); +} + +#[test] +fn successful_migration() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-incentives", "1.0.0").unwrap(); + + let old_owner = "spiderman_246"; + v1_state::OWNER + .save( + deps.as_mut().storage, + &v1_state::OwnerState::B(OwnerSetNoneProposed { + owner: Addr::unchecked(old_owner), + }), + ) + .unwrap(); + + let mars_denom = "umars"; + let old_config = mars_red_bank_types_old::incentives::Config { + address_provider: Addr::unchecked("address_provider"), + mars_denom: mars_denom.to_string(), + }; + v1_state::CONFIG.save(deps.as_mut().storage, &old_config).unwrap(); + + let atom_denom = "uatom"; + let usdc_denom = "uusdc"; + let osmo_denom = "uosmo"; + + let incentive_start_time = 500_000u64; + let duration = 864_000u64; // 10 days + let migration_time = incentive_start_time + duration + 100u64; + + // The incentive will have to be recalculated for the entire duration + let atom_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(100), + start_time: incentive_start_time, + duration, + index: Decimal::one(), + last_updated: incentive_start_time, + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, atom_denom, &atom_incentive).unwrap(); + + // The incentive will have to be recalculated for the part of the duration + let usdc_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(50), + start_time: incentive_start_time, + duration, + index: Decimal::from_ratio(12u128, 10u128), + last_updated: incentive_start_time + 86400u64, // + 1 day + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, usdc_denom, &usdc_incentive).unwrap(); + + // The incentive won't be recalculated because it finished before migration time + let osmo_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(50), + start_time: incentive_start_time, + duration, + index: Decimal::from_ratio(15u128, 10u128), + last_updated: migration_time - 10u64, + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, osmo_denom, &osmo_incentive).unwrap(); + + // Set user asset indices for all incentive assets + let user_1 = Addr::unchecked("user_1"); + let user_1_atom_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, atom_denom), &user_1_atom_idx_old) + .unwrap(); + let user_1_usdc_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, usdc_denom), &user_1_usdc_idx_old) + .unwrap(); + let user_1_osmo_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, osmo_denom), &user_1_osmo_idx_old) + .unwrap(); + + // Set user asset indices only for osmo. Index is up to date with asset incentive index. No rewards accured. + let user_2 = Addr::unchecked("user_2"); + let user_2_osmo_idx_old = osmo_incentive.index; + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_2, osmo_denom), &user_2_osmo_idx_old) + .unwrap(); + + // Set user asset indices only for atom + let user_3 = Addr::unchecked("user_3"); + let user_3_atom_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_3, atom_denom), &user_3_atom_idx_old) + .unwrap(); + + // Set unclaimed rewards only for user_1. + // user_2 doesn't accrue any new rewards because osmo incentive finished before migration time. + // user_3 not set in order to check if new state creation works for him. + let user_1_unclaimed_rewards = Uint128::new(1000); + v1_state::USER_UNCLAIMED_REWARDS + .save(deps.as_mut().storage, &user_1, &user_1_unclaimed_rewards) + .unwrap(); + + // Setup markets + let atom_collateral_total_scaled = Uint128::new(100_000_000); + deps.querier.set_redbank_market(create_market(atom_denom, atom_collateral_total_scaled)); + let usdc_collateral_total_scaled = Uint128::new(1_250_000_000); + deps.querier.set_redbank_market(create_market(usdc_denom, usdc_collateral_total_scaled)); + let osmo_collateral_total_scaled = Uint128::new(520_000_000); + deps.querier.set_redbank_market(create_market(osmo_denom, osmo_collateral_total_scaled)); + + // Setup atom collaterals. Sum of all positions should be equal to atom_collateral_total_scaled. + let user_1_atom_amount_scaled = Uint128::zero(); // Setting zero to check if user_1 index is updated correctly + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(atom_denom, user_1_atom_amount_scaled), + ); + let user_3_atom_amount_scaled = atom_collateral_total_scaled; + deps.querier.set_red_bank_user_collateral( + &user_3, + create_user_collateral(atom_denom, user_3_atom_amount_scaled), + ); + + // Setup usdc collaterals. Sum of all positions should be equal to usdc_collateral_total_scaled + let user_1_usdc_amount_scaled = usdc_collateral_total_scaled; + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(usdc_denom, user_1_usdc_amount_scaled), + ); + + // Setup osmo collaterals. Sum of all positions should be equal to osmo_collateral_total_scaled + let user_1_osmo_amount_scaled = Uint128::new(120_000_000); + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(osmo_denom, user_1_osmo_amount_scaled), + ); + let user_2_osmo_amount_scaled = Uint128::new(400_000_000); + deps.querier.set_red_bank_user_collateral( + &user_2, + create_user_collateral(osmo_denom, user_2_osmo_amount_scaled), + ); + + let env = mars_testing::mock_env(MockEnvParams { + block_time: Timestamp::from_seconds(migration_time), + ..Default::default() + }); + + let epoch_duration = 604800; + let max_whitelisted_denoms = 12; + let res = migrate( + deps.as_mut(), + env, + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration, + max_whitelisted_denoms, + }), + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + assert_eq!(res.events, vec![] as Vec); + assert!(res.data.is_none()); + assert_eq!( + res.attributes, + vec![attr("action", "migrate"), attr("from_version", "1.0.0"), attr("to_version", "2.0.0")] + ); + + let o = OWNER.query(deps.as_ref().storage).unwrap(); + assert_eq!(old_owner.to_string(), o.owner.unwrap()); + assert!(o.proposed.is_none()); + assert!(o.initialized); + assert!(!o.abolished); + assert!(o.emergency_owner.is_none()); + + let new_config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!( + new_config, + Config { + address_provider: old_config.address_provider, + max_whitelisted_denoms + } + ); + + let whitelist_count = WHITELIST_COUNT.load(deps.as_ref().storage).unwrap(); + assert_eq!(whitelist_count, 1); + let whitelist = WHITELIST + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(whitelist.len(), 1); + assert_eq!(whitelist.get("umars").unwrap(), &Uint128::one()); + + // Update asset incentive indices and check if indices changed + let mut new_atom_incentive = atom_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_atom_incentive, + atom_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_ne!(atom_incentive.index, new_atom_incentive.index); + let mut new_usdc_incentive = usdc_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_usdc_incentive, + usdc_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_ne!(usdc_incentive.index, new_usdc_incentive.index); + let mut new_osmo_incentive = osmo_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_osmo_incentive, + osmo_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_eq!(osmo_incentive.index, new_osmo_incentive.index); // should be equal because last_updated is after incentive end time + + // Check if incentive states are updated correctly + let incentive_states = INCENTIVE_STATES + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(incentive_states.len(), 3); + assert_eq!( + incentive_states.get(&(atom_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_atom_incentive.index, + last_updated: migration_time + } + ); + assert_eq!( + incentive_states.get(&(usdc_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_usdc_incentive.index, + last_updated: migration_time + } + ); + assert_eq!( + incentive_states.get(&(osmo_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_osmo_incentive.index, + last_updated: migration_time + } + ); + + // Check if user asset indices are updated correctly + let user_1_atom_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_atom_idx, new_atom_incentive.index); + let user_1_usdc_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), usdc_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_usdc_idx, new_usdc_incentive.index); + let user_1_osmo_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_osmo_idx, new_osmo_incentive.index); + + let user_2_osmo_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_2, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_2_osmo_idx, new_osmo_incentive.index); + + let user_3_atom_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_3, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_3_atom_idx, new_atom_incentive.index); + + // Check if user unclaimed rewards are migrated correctly + let user_1_atom_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_atom_amount_scaled, + user_1_atom_idx_old, + new_atom_incentive.index, + ) + .unwrap(); + let user_1_atom_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_atom_rewards_migrated, user_1_unclaimed_rewards + user_1_atom_rewards); + let user_1_usdc_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_usdc_amount_scaled, + user_1_usdc_idx_old, + new_usdc_incentive.index, + ) + .unwrap(); + let user_1_usdc_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), usdc_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_usdc_rewards_migrated, user_1_usdc_rewards); + let user_1_osmo_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_osmo_amount_scaled, + user_1_osmo_idx_old, + new_osmo_incentive.index, + ) + .unwrap(); + let user_1_osmo_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_osmo_rewards_migrated, user_1_osmo_rewards); + + let user_2_osmo_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_2_osmo_amount_scaled, + user_2_osmo_idx_old, + new_osmo_incentive.index, + ) + .unwrap(); + assert_eq!(user_2_osmo_rewards, Uint128::zero()); + + let user_3_atom_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_3_atom_amount_scaled, + user_3_atom_idx_old, + new_atom_incentive.index, + ) + .unwrap(); + let user_3_atom_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_3, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_3_atom_rewards_migrated, user_3_atom_rewards); +} + +fn create_market(denom: &str, scaled_amt: Uint128) -> Market { + Market { + denom: denom.to_string(), + collateral_total_scaled: scaled_amt, + ..Default::default() + } +} + +fn create_user_collateral(denom: &str, scaled_amt: Uint128) -> UserCollateralResponse { + UserCollateralResponse { + denom: denom.to_string(), + amount_scaled: scaled_amt, + amount: Uint128::zero(), // doesn't matter for this test + enabled: true, + } +} diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index 8ffa77965..a74db63d1 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -114,7 +114,7 @@ pub mod v1_0_1 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.2.0") + attr("to_version", "2.0.0") ] ); @@ -149,7 +149,7 @@ pub mod v1_0_1 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.2.0") + attr("to_version", "2.0.0") ] ); diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs index c94cad794..db37b8375 100644 --- a/packages/types/src/incentives.rs +++ b/packages/types/src/incentives.rs @@ -252,7 +252,19 @@ pub enum QueryMsg { } #[cw_serde] -pub struct MigrateMsg {} +pub struct V2Updates { + /// The amount of time in seconds for each incentive epoch. This is the minimum amount of time + /// that an incentive can last, and each incentive must be a multiple of this duration. + pub epoch_duration: u64, + /// The maximum number of incentive denoms that can be whitelisted at any given time. This is + /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. + pub max_whitelisted_denoms: u8, +} + +#[cw_serde] +pub enum MigrateMsg { + V1_0_0ToV2_0_0(V2Updates), +} #[cw_serde] pub struct EmissionResponse { diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index 34ce77c5b..a8ce16fdd 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -1,6 +1,6 @@ { "contract_name": "mars-address-provider", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index ae8071490..d2d6badd1 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -1,6 +1,6 @@ { "contract_name": "mars-incentives", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 1a1ef011e..a6324eacb 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-osmosis", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-oracle-wasm/mars-oracle-wasm.json b/schemas/mars-oracle-wasm/mars-oracle-wasm.json index 19d235a88..c5e3474dd 100644 --- a/schemas/mars-oracle-wasm/mars-oracle-wasm.json +++ b/schemas/mars-oracle-wasm/mars-oracle-wasm.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-wasm", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json index dbf0f6338..2350dddbc 100644 --- a/schemas/mars-params/mars-params.json +++ b/schemas/mars-params/mars-params.json @@ -1,6 +1,6 @@ { "contract_name": "mars-params", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 6bafea4e5..d38a7dd5f 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -1,6 +1,6 @@ { "contract_name": "mars-red-bank", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index 30e68fddc..c9f4e9ce1 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -1,6 +1,6 @@ { "contract_name": "mars-rewards-collector-base", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-swapper-astroport/mars-swapper-astroport.json b/schemas/mars-swapper-astroport/mars-swapper-astroport.json index 93ef8f7c2..778ee5515 100644 --- a/schemas/mars-swapper-astroport/mars-swapper-astroport.json +++ b/schemas/mars-swapper-astroport/mars-swapper-astroport.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-astroport", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json index 512d773a2..6d4b6a009 100644 --- a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json +++ b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-osmosis", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#",