diff --git a/Cargo.lock b/Cargo.lock index 3f3024b..d240452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "dexter-vault" -version = "1.2.0" +version = "1.2.1" dependencies = [ "const_format", "cosmwasm-schema", diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index ca8de46..54478d1 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -5,5 +5,5 @@ c6bb97648dfef5c69d42924d48dc579bb5db0c4fc0514cee1d54ee387a657a05 dexter_lp_toke 520307ff5f915ad232cafb3b3a14747491bef37e3cd1e2b630d9ad0cb915023f dexter_router.wasm 030b85563cefa2f87246cf383498220fa577ae7e5711138a69f6ce6e5839f792 dexter_stable_pool.wasm 3acb65a778fc3f467d29a0e22a5c093b78648b893a18e5b611db25e8c8368587 dexter_superfluid_lp.wasm -036795694b5d5d2ed947c457219f09b9de3db52a86a1f90b1cc2284ee3d6c90c dexter_vault.wasm +eb6f3e2e731d83d0a840fb1bb6e09271ce4f435ccd64e7f93858b6ad9f0aaeef dexter_vault.wasm 899428140866ec5249b9382f8e13f1874394f9bbd1beee9fd5f020b8ba1d82fd dexter_weighted_pool.wasm diff --git a/artifacts/dexter_vault.wasm b/artifacts/dexter_vault.wasm index d879768..f3efe4c 100644 Binary files a/artifacts/dexter_vault.wasm and b/artifacts/dexter_vault.wasm differ diff --git a/contracts/vault/Cargo.toml b/contracts/vault/Cargo.toml index ba78c85..2caf56f 100644 --- a/contracts/vault/Cargo.toml +++ b/contracts/vault/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dexter-vault" -version = "1.2.0" +version = "1.2.1" authors = ["Persistence Labs"] edition = "2021" description = "Dexter Factory contract - entry point to create new pools. Maintains directory for all pools" diff --git a/contracts/vault/src/contract.rs b/contracts/vault/src/contract.rs index f8958a6..022d157 100644 --- a/contracts/vault/src/contract.rs +++ b/contracts/vault/src/contract.rs @@ -42,6 +42,7 @@ const CONTRACT_NAME: &str = "dexter-vault"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const CONTRACT_VERSION_V1: &str = "1.0.0"; const CONTRACT_VERSION_V1_1: &str = "1.1.0"; +const CONTRACT_VERSION_V1_2: &str = "1.2.0"; /// A `reply` call code ID of sub-message. const INSTANTIATE_LP_REPLY_ID: u64 = 1; @@ -2157,6 +2158,27 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + // validate contract name + if contract_version.contract != CONTRACT_NAME { + return Err(ContractError::InvalidContractNameForMigration { + expected: CONTRACT_NAME.to_string(), + actual: contract_version.contract, + }); + } + + // validate that current version is v1.2 + if contract_version.version != CONTRACT_VERSION_V1_2 { + return Err(ContractError::InvalidContractVersionForUpgrade { + upgrade_version: CONTRACT_VERSION.to_string(), + expected: CONTRACT_VERSION_V1_2.to_string(), + actual: contract_version.version, + }); + } + + // No state changes needed for this migration - just the overflow fix in calculate_proportional_refund + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + } } Ok(Response::new() @@ -2567,12 +2589,7 @@ fn calculate_proportional_refund( let mut refund_assets = Vec::new(); for asset in pool_assets { - let refund_amount = asset - .amount - .checked_mul(user_lp_tokens) - .map_err(|e| ContractError::Std(StdError::overflow(e)))? - .checked_div(total_lp_tokens) - .map_err(|e| ContractError::Std(StdError::divide_by_zero(e)))?; + let refund_amount = asset.amount.multiply_ratio(user_lp_tokens, total_lp_tokens); if !refund_amount.is_zero() { refund_assets.push(Asset { diff --git a/contracts/vault/tests/defunct_pool.rs b/contracts/vault/tests/defunct_pool.rs index 04918bf..9aa6bce 100644 --- a/contracts/vault/tests/defunct_pool.rs +++ b/contracts/vault/tests/defunct_pool.rs @@ -1,7 +1,10 @@ use cosmwasm_std::{coins, Addr, Uint128}; use cw_multi_test::Executor; use dexter::asset::{Asset, AssetInfo}; -use dexter::vault::{DefunctPoolInfo, ExecuteMsg, QueryMsg}; +use dexter::vault::{DefunctPoolInfo, ExecuteMsg, QueryMsg, ConfigResponse, PoolInfoResponse, PoolType}; +use dexter::uint128_with_precision; +use cosmwasm_std::Decimal; +use cosmwasm_std::to_json_binary; pub mod utils; @@ -1544,3 +1547,299 @@ fn test_defunct_pool_refund_includes_unclaimed_rewards() { println!(" - Unclaimed rewards: Remain in multistaking and must be withdrawn separately"); } +#[test] +fn test_defunct_pool_refund_with_large_18_decimal_values() { + let owner = Addr::unchecked("owner"); + let mut app = utils::mock_app(owner.clone(), vec![ + cosmwasm_std::Coin { + denom: "denom1".to_string(), + amount: Uint128::from(100_000_000_000u128), + }, + cosmwasm_std::Coin { + denom: "denom2".to_string(), + amount: Uint128::from(100_000_000_000u128), + }, + cosmwasm_std::Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000_000_000u128), + }, + ]); + let vault_instance = utils::instantiate_contract(&mut app, &owner); + + // Initialize the token contracts first + let (token1, token2, token3) = utils::initialize_3_tokens(&mut app, &owner); + + // Mint tokens with reasonable amounts for testing + let mint_amount = Uint128::from(1_000_000_000_000u128); // 1e12 + + utils::mint_some_tokens( + &mut app, + owner.clone(), + token1.clone(), + mint_amount, + owner.to_string(), + ); + utils::mint_some_tokens( + &mut app, + owner.clone(), + token2.clone(), + mint_amount, + owner.to_string(), + ); + utils::mint_some_tokens( + &mut app, + owner.clone(), + token3.clone(), + mint_amount, + owner.to_string(), + ); + + utils::increase_token_allowance( + &mut app, + owner.clone(), + token1.clone(), + vault_instance.to_string(), + mint_amount, + ); + utils::increase_token_allowance( + &mut app, + owner.clone(), + token2.clone(), + vault_instance.to_string(), + mint_amount, + ); + utils::increase_token_allowance( + &mut app, + owner.clone(), + token3.clone(), + vault_instance.to_string(), + mint_amount, + ); + + let (_, lp_token_instance, pool_id) = utils::initialize_weighted_pool( + &mut app, + &owner, + vault_instance.clone(), + token1.clone(), + token2.clone(), + token3.clone(), + "denom1".to_string(), + "denom2".to_string(), + ); + + // Join pool with reasonable amounts + let join_amount = Uint128::from(1_000_000u128); // 1e6 + let join_msg = ExecuteMsg::JoinPool { + pool_id, + recipient: None, + assets: Some(vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "denom1".to_string(), + }, + amount: join_amount, + }, + Asset { + info: AssetInfo::NativeToken { + denom: "denom2".to_string(), + }, + amount: join_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token2.clone(), + }, + amount: join_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token1.clone(), + }, + amount: join_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token3.clone(), + }, + amount: join_amount, + }, + ]), + min_lp_to_receive: None, + auto_stake: None, + }; + + let result = app.execute_contract(owner.clone(), vault_instance.clone(), &join_msg, &[ + cosmwasm_std::Coin { + denom: "denom1".to_string(), + amount: join_amount, + }, + cosmwasm_std::Coin { + denom: "denom2".to_string(), + amount: join_amount, + }, + ]); + assert!(result.is_ok(), "Join pool should succeed"); + + // Make the pool defunct + let defunct_msg = ExecuteMsg::DefunctPool { pool_id }; + let result = app.execute_contract(owner.clone(), vault_instance.clone(), &defunct_msg, &[]); + assert!(result.is_ok(), "Defunct pool should succeed"); + + // Create a user with a large LP token balance that would cause overflow in the old implementation + let user = Addr::unchecked("user_with_large_balance"); + + // Use the exact values from the production error + let user_lp_balance = Uint128::from(49542085906941706126u128); // The exact amount from the error + let _asset_amount = Uint128::from(3751295874309656386815u128); // The exact asset amount from the error + + // Mint LP tokens to the user + utils::mint_some_tokens( + &mut app, + owner.clone(), + lp_token_instance.clone(), + user_lp_balance, + user.to_string(), + ); + + // Process refund batch with the user who has large LP tokens + // This should NOT overflow with the multiply_ratio fix + let refund_msg = ExecuteMsg::ProcessRefundBatch { + pool_id, + user_addresses: vec![user.to_string()], + }; + + let result = app.execute_contract(owner.clone(), vault_instance.clone(), &refund_msg, &[]); + + // This should succeed with the multiply_ratio fix, but would fail with overflow in the old implementation + assert!(result.is_ok(), "Refund batch should succeed with large values: {:?}", result.unwrap_err()); + + // Verify the user is marked as refunded + let is_refunded: bool = app + .wrap() + .query_wasm_smart( + &vault_instance, + &QueryMsg::IsUserRefunded { + pool_id, + user: user.to_string(), + }, + ) + .unwrap(); + + assert!(is_refunded, "User should be marked as refunded"); + + // Verify the refund amounts are reasonable (not zero, not overflowed) + let defunct_pool_info: Option = app + .wrap() + .query_wasm_smart(&vault_instance, &QueryMsg::GetDefunctPoolInfo { pool_id }) + .unwrap(); + + let defunct_pool_info = defunct_pool_info.unwrap(); + + // Check that some assets were refunded (current assets should be less than total assets) + for (total_asset, current_asset) in defunct_pool_info.total_assets_at_defunct.iter() + .zip(defunct_pool_info.current_assets_in_pool.iter()) { + assert!(current_asset.amount < total_asset.amount, + "Current asset amount should be less than total asset amount after refund"); + assert!(!current_asset.amount.is_zero(), + "Current asset amount should not be zero after refund"); + } +} + +#[test] +fn test_defunct_pool_refund_with_large_18_decimal_values_18dec() { + let owner = Addr::unchecked("owner"); + let mut app = utils::mock_app(owner.clone(), vec![]); + let vault_instance = utils::instantiate_contract(&mut app, &owner); + + // Initialize 3 tokens with 18 decimals + let token_decimals = 18u8; + let (token1, token2, token3) = utils::initialize_3_tokens_with_decimals(&mut app, &owner, token_decimals); + + // Mint large 18-decimal amounts to owner + let mint_amount = uint128_with_precision!(1_000_000u128, 18); // 1 million tokens + utils::mint_some_tokens(&mut app, owner.clone(), token1.clone(), mint_amount, owner.to_string()); + utils::mint_some_tokens(&mut app, owner.clone(), token2.clone(), mint_amount, owner.to_string()); + utils::mint_some_tokens(&mut app, owner.clone(), token3.clone(), mint_amount, owner.to_string()); + + utils::increase_token_allowance(&mut app, owner.clone(), token1.clone(), vault_instance.to_string(), mint_amount); + utils::increase_token_allowance(&mut app, owner.clone(), token2.clone(), vault_instance.to_string(), mint_amount); + utils::increase_token_allowance(&mut app, owner.clone(), token3.clone(), vault_instance.to_string(), mint_amount); + + // Create a weighted pool with only CW20 tokens (no native) + let asset_infos = vec![ + AssetInfo::Token { contract_addr: token1.clone() }, + AssetInfo::Token { contract_addr: token2.clone() }, + AssetInfo::Token { contract_addr: token3.clone() }, + ]; + let asset_weights = vec![ + Asset { info: AssetInfo::Token { contract_addr: token1.clone() }, amount: uint128_with_precision!(20u128, 18) }, + Asset { info: AssetInfo::Token { contract_addr: token2.clone() }, amount: uint128_with_precision!(20u128, 18) }, + Asset { info: AssetInfo::Token { contract_addr: token3.clone() }, amount: uint128_with_precision!(20u128, 18) }, + ]; + let vault_config_res: ConfigResponse = app.wrap().query_wasm_smart(vault_instance.clone(), &QueryMsg::Config {}).unwrap(); + let next_pool_id = vault_config_res.next_pool_id; + let msg = ExecuteMsg::CreatePoolInstance { + pool_type: PoolType::Weighted {}, + asset_infos: asset_infos.clone(), + native_asset_precisions: vec![], + init_params: Some( + to_json_binary(&dexter_weighted_pool::state::WeightedParams { + weights: asset_weights, + exit_fee: Some(Decimal::from_ratio(1u128, 100u128)), + }).unwrap(), + ), + fee_info: None, + }; + app.execute_contract(owner.clone(), vault_instance.clone(), &msg, &[]).unwrap(); + let pool_info_res: PoolInfoResponse = app.wrap().query_wasm_smart(vault_instance.clone(), &QueryMsg::GetPoolById { pool_id: next_pool_id }).unwrap(); + let lp_token_instance = pool_info_res.lp_token_addr; + let pool_id = pool_info_res.pool_id; + + // Join pool with 18 decimal assets + let join_amount = uint128_with_precision!(1_000u128, 18); + let join_msg = ExecuteMsg::JoinPool { + pool_id, + recipient: None, + assets: Some(vec![ + Asset { info: AssetInfo::Token { contract_addr: token1.clone() }, amount: join_amount }, + Asset { info: AssetInfo::Token { contract_addr: token2.clone() }, amount: join_amount }, + Asset { info: AssetInfo::Token { contract_addr: token3.clone() }, amount: join_amount }, + ]), + min_lp_to_receive: None, + auto_stake: None, + }; + let result = app.execute_contract(owner.clone(), vault_instance.clone(), &join_msg, &[]); + assert!(result.is_ok(), "Join pool should succeed"); + + // Make the pool defunct + let defunct_msg = ExecuteMsg::DefunctPool { pool_id }; + let result = app.execute_contract(owner.clone(), vault_instance.clone(), &defunct_msg, &[]); + assert!(result.is_ok(), "Defunct pool should succeed"); + + // Mint a large 18-decimal LP balance to a user (simulate prod overflow) + let user = Addr::unchecked("user_with_large_balance"); + let user_lp_balance = uint128_with_precision!(49_542_085_906_941_706_126u128, 0); // as in prod error + utils::mint_some_tokens(&mut app, vault_instance.clone(), lp_token_instance.clone(), user_lp_balance, user.to_string()); + + // Process refund batch for the user + let refund_msg = ExecuteMsg::ProcessRefundBatch { + pool_id, + user_addresses: vec![user.to_string()], + }; + let result = app.execute_contract(owner.clone(), vault_instance.clone(), &refund_msg, &[]); + assert!(result.is_ok(), "Refund batch should succeed with large values"); + + // Verify the user is marked as refunded + let is_refunded: bool = app + .wrap() + .query_wasm_smart( + &vault_instance, + &QueryMsg::IsUserRefunded { + pool_id, + user: user.to_string(), + }, + ) + .unwrap(); + assert!(is_refunded, "User should be marked as refunded"); +} + diff --git a/contracts/vault/tests/utils/mod.rs b/contracts/vault/tests/utils/mod.rs index c622724..1acb6eb 100644 --- a/contracts/vault/tests/utils/mod.rs +++ b/contracts/vault/tests/utils/mod.rs @@ -229,6 +229,71 @@ pub fn initialize_3_tokens(app: &mut App, owner: &Addr) -> (Addr, Addr, Addr) { (token_instance0, token_instance2, token_instance3) } +pub fn initialize_3_tokens_with_decimals(app: &mut App, owner: &Addr, decimals: u8) -> (Addr, Addr, Addr) { + let token_code_id = store_token_code(app); + let token_instance0 = app + .instantiate_contract( + token_code_id, + Addr::unchecked(owner.clone()), + &TokenInstantiateMsg { + name: "x_token".to_string(), + symbol: "X-Tok".to_string(), + decimals, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }, + &[], + "x_token", + None, + ) + .unwrap(); + let token_instance1 = app + .instantiate_contract( + token_code_id, + Addr::unchecked(owner.clone()), + &TokenInstantiateMsg { + name: "y_token".to_string(), + symbol: "y-Tok".to_string(), + decimals, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }, + &[], + "y_token", + None, + ) + .unwrap(); + let token_instance2 = app + .instantiate_contract( + token_code_id, + Addr::unchecked(owner.clone()), + &TokenInstantiateMsg { + name: "z_token".to_string(), + symbol: "z-Tok".to_string(), + decimals, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }, + &[], + "z_token", + None, + ) + .unwrap(); + (token_instance0, token_instance1, token_instance2) +} + // Mints some Tokens to "to" recipient pub fn mint_some_tokens( app: &mut App, diff --git a/packages/dexter/src/vault.rs b/packages/dexter/src/vault.rs index f7f22aa..33dd581 100644 --- a/packages/dexter/src/vault.rs +++ b/packages/dexter/src/vault.rs @@ -484,7 +484,9 @@ pub enum MigrateMsg { V1_2 { /// List of reward assets to check when validating reward schedules during defunct operations reward_schedule_validation_assets: Option>, - } + }, + /// Migration for overflow fix in defunct pool refund calculations + V1_2_1 {} } // ----------------x----------------x----------------x----------------x----------------x----------------