From 79a850ae936fe3dc00b898073a578deee9921da3 Mon Sep 17 00:00:00 2001 From: Murad Karammaev Date: Thu, 29 Feb 2024 10:13:14 +0200 Subject: [PATCH 1/2] feat: unbonding finalisation --- contracts/core/src/contract.rs | 191 ++- contracts/core/src/error.rs | 2 +- contracts/core/src/tests.rs | 13 +- contracts/puppeteer/src/contract.rs | 9 +- contracts/strategy/src/tests.rs | 5 +- contracts/withdrawal-manager/src/contract.rs | 4 +- contracts/withdrawal-manager/src/error.rs | 4 +- .../neutron-query-relayer/build.sh | 4 +- integration_tests/package.json | 3 +- .../src/generated/contractLib/lidoCore.ts | 283 +--- .../src/generated/contractLib/lidoFactory.ts | 149 +- integration_tests/src/testSuite.ts | 19 +- .../src/testcases/auto-withdrawer.test.ts | 959 +++++++++-- .../src/testcases/core.fsm.test.ts | 1209 -------------- integration_tests/src/testcases/core.test.ts | 1415 +++++++++++++---- packages/base/src/msg/core.rs | 16 +- packages/base/src/msg/puppeteer.rs | 7 +- packages/base/src/state/core.rs | 25 +- packages/base/src/state/puppeteer.rs | 6 +- packages/puppeteer-base/src/proto.rs | 1 + packages/puppeteer-base/src/sudo.rs | 7 +- 21 files changed, 2201 insertions(+), 2130 deletions(-) delete mode 100644 integration_tests/src/testcases/core.fsm.test.ts diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index 0b43c706..11a5f907 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -9,10 +9,10 @@ use cw2::set_contract_version; use lido_helpers::answer::response; use lido_puppeteer_base::msg::TransferReadyBatchMsg; use lido_staking_base::state::core::{ - Config, ConfigOptional, ContractState, FeeItem, NonNativeRewardsItem, UnbondBatch, - UnbondBatchStatus, UnbondItem, COLLECTED_FEES, CONFIG, FAILED_BATCH_ID, FSM, + unbond_batches_map, Config, ConfigOptional, ContractState, FeeItem, NonNativeRewardsItem, + UnbondBatch, UnbondBatchStatus, UnbondItem, COLLECTED_FEES, CONFIG, FAILED_BATCH_ID, FSM, LAST_ICA_BALANCE_CHANGE_HEIGHT, LAST_PUPPETEER_RESPONSE, NON_NATIVE_REWARDS_CONFIG, - PENDING_TRANSFER, PRE_UNBONDING_BALANCE, TOTAL_LSM_SHARES, UNBOND_BATCHES, UNBOND_BATCH_ID, + PENDING_TRANSFER, PRE_UNBONDING_BALANCE, TOTAL_LSM_SHARES, UNBOND_BATCH_ID, }; use lido_staking_base::state::validatorset::ValidatorInfo; use lido_staking_base::state::withdrawal_voucher::{Metadata, Trait}; @@ -26,7 +26,6 @@ use lido_staking_base::{ }; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; use prost::Message; -use std::str::FromStr; use std::vec; pub type MessageWithFeeResponse = (CosmosMsg, Option>); @@ -53,7 +52,7 @@ pub fn instantiate( CONFIG.save(deps.storage, &msg.into())?; //an empty unbonding batch added as it's ready to be used on unbond action UNBOND_BATCH_ID.save(deps.storage, &0)?; - UNBOND_BATCHES.save(deps.storage, 0, &new_unbond(env.block.time.seconds()))?; + unbond_batches_map().save(deps.storage, 0, &new_unbond(env.block.time.seconds()))?; FSM.set_initial_state(deps.storage, ContractState::Idle)?; LAST_IDLE_CALL.save(deps.storage, &0)?; LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &0)?; @@ -107,20 +106,20 @@ fn query_exchange_rate( .sum(); let mut batch_id = UNBOND_BATCH_ID.load(deps.storage)?; let mut unprocessed_unbonded_amount = Uint128::zero(); - let batch = UNBOND_BATCHES.load(deps.storage, batch_id)?; + let batch = unbond_batches_map().load(deps.storage, batch_id)?; if batch.status == UnbondBatchStatus::New { unprocessed_unbonded_amount += batch.total_amount; } if batch_id > 0 { batch_id -= 1; - let batch = UNBOND_BATCHES.load(deps.storage, batch_id)?; + let batch = unbond_batches_map().load(deps.storage, batch_id)?; if batch.status == UnbondBatchStatus::UnbondRequested { unprocessed_unbonded_amount += batch.total_amount; } } let failed_batch_id = FAILED_BATCH_ID.may_load(deps.storage)?; if let Some(failed_batch_id) = failed_batch_id { - let failed_batch = UNBOND_BATCHES.load(deps.storage, failed_batch_id)?; + let failed_batch = unbond_batches_map().load(deps.storage, failed_batch_id)?; unprocessed_unbonded_amount += failed_batch.total_amount; } let core_balance = deps @@ -130,7 +129,9 @@ fn query_exchange_rate( let extra_amount = match FSM.get_current_state(deps.storage)? { ContractState::Transfering => PENDING_TRANSFER.load(deps.storage), ContractState::Staking => { - let (ica_balance, _) = get_ica_balance_by_denom( + // FIXME: this ICA balance should be fresh in order to guarantee correct + // exchange rate calculation + let (ica_balance, _, _) = get_ica_balance_by_denom( deps, &config.puppeteer_contract, &config.remote_denom, @@ -150,7 +151,7 @@ fn query_exchange_rate( } fn query_unbond_batch(deps: Deps, batch_id: Uint128) -> StdResult { - to_json_binary(&UNBOND_BATCHES.load(deps.storage, batch_id.into())?) + to_json_binary(&unbond_batches_map().load(deps.storage, batch_id.u128())?) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -175,10 +176,6 @@ pub fn execute( ExecuteMsg::UpdateNonNativeRewardsReceivers { items } => { execute_set_non_native_rewards_receivers(deps, env, info, items) } - ExecuteMsg::FakeProcessBatch { - batch_id, - unbonded_amount, - } => execute_fake_process_batch(deps, env, info, batch_id, unbonded_amount), ExecuteMsg::Tick {} => execute_tick(deps, env, info), ExecuteMsg::PuppeteerHook(msg) => execute_puppeteer_hook(deps, env, info, *msg), } @@ -248,6 +245,7 @@ fn execute_tick_idle( let mut attrs = vec![attr("action", "tick_idle")]; let last_idle_call = LAST_IDLE_CALL.load(deps.storage)?; let mut messages = vec![]; + if env.block.time.seconds() - last_idle_call < config.idle_min_interval { //process non-native rewards if let Some(transfer_msg) = @@ -262,8 +260,8 @@ fn execute_tick_idle( ensure!( !is_unbonding_time_close( deps.as_ref(), - &env.block.time.seconds(), - &config.unbonding_safe_period + env.block.time.seconds(), + config.unbonding_safe_period )?, ContractError::UnbondingTimeIsClose {} ); @@ -271,16 +269,62 @@ fn execute_tick_idle( .pump_address .clone() .ok_or(ContractError::PumpAddressIsNotSet {})?; + + // detect already unbonded batch, if there is one + let unbonding_batches = unbond_batches_map() + .idx + .status + .prefix(UnbondBatchStatus::Unbonding as u8) + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + if !unbonding_batches.is_empty() { + let (id, mut unbonding_batch) = unbonding_batches + .into_iter() + // we only need the oldest Unbonding batch + .min_by_key(|(_, batch)| batch.expected_release) + // this `unwrap()` is safe to call since in this branch + // `unbonding_batches` is not empty + .unwrap(); + let (balance, _local_height, ica_balance_local_time) = get_ica_balance_by_denom( + deps.as_ref(), + &config.puppeteer_contract, + &config.remote_denom, + true, + )?; + + if unbonding_batch.expected_release <= env.block.time.seconds() + && unbonding_batch.expected_release < ica_balance_local_time + { + let (unbonded_amount, slashing_effect) = + if balance < unbonding_batch.expected_amount { + ( + balance, + Decimal::from_ratio(balance, unbonding_batch.expected_amount), + ) + } else { + (unbonding_batch.expected_amount, Decimal::one()) + }; + unbonding_batch.unbonded_amount = Some(unbonded_amount); + unbonding_batch.slashing_effect = Some(slashing_effect); + unbonding_batch.status = UnbondBatchStatus::Unbonded; + unbond_batches_map().save(deps.storage, id, &unbonding_batch)?; + } + } + // process unbond if any already unbonded // and claim rewards let transfer: Option = match get_unbonded_batch(deps.as_ref())? { - Some((batch_id, batch)) => Some(TransferReadyBatchMsg { - batch_id, - amount: batch - .unbonded_amount - .ok_or(ContractError::UnbondedAmountIsNotSet {})?, - recipient: pump_address, - }), + Some((batch_id, mut batch)) => { + batch.status = UnbondBatchStatus::Withdrawing; + unbond_batches_map().save(deps.storage, batch_id, &batch)?; + Some(TransferReadyBatchMsg { + batch_id, + amount: batch + .unbonded_amount + .ok_or(ContractError::UnbondedAmountIsNotSet {})?, + recipient: pump_address, + }) + } None => None, }; @@ -289,7 +333,7 @@ fn execute_tick_idle( &lido_staking_base::msg::validatorset::QueryMsg::Validators {}, )?; - let (delegations, _) = deps + let (delegations, _, _) = deps .querier .query_wasm_smart::( config.puppeteer_contract.to_string(), @@ -308,6 +352,7 @@ fn execute_tick_idle( .filter(|d| validators_map.get(&d.validator).map_or(false, |_| true)) .map(|d| d.validator.clone()) .collect::>(); + // XXX: maybe this state transition should only happen if `!validators_to_claim.is_empty()`? FSM.go_to(deps.storage, ContractState::Claiming)?; if validators_to_claim.is_empty() { attrs.push(attr("validators_to_claim", "empty")); @@ -325,17 +370,17 @@ fn execute_tick_idle( } else { attrs.push(attr("validators_to_claim", validators_to_claim.join(","))); messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.puppeteer_contract.to_string(), - msg: to_json_binary( - &lido_staking_base::msg::puppeteer::ExecuteMsg::ClaimRewardsAndOptionalyTransfer { - validators: validators_to_claim, - transfer, - timeout: Some(config.puppeteer_timeout), - reply_to: env.contract.address.to_string(), - }, - )?, - funds: info.funds, - })); + contract_addr: config.puppeteer_contract.clone(), + msg: to_json_binary( + &lido_staking_base::msg::puppeteer::ExecuteMsg::ClaimRewardsAndOptionalyTransfer { + validators: validators_to_claim, + transfer, + timeout: Some(config.puppeteer_timeout), + reply_to: env.contract.address.to_string(), + }, + )?, + funds: info.funds, + })); } attrs.push(attr("state", "claiming")); LAST_IDLE_CALL.save(deps.storage, &env.block.time.seconds())?; @@ -361,11 +406,12 @@ fn execute_tick_claiming( .. } => { if let Some(transfer) = transfer { - let mut batch = UNBOND_BATCHES.load(deps.storage, transfer.batch_id)?; + let mut batch = + unbond_batches_map().load(deps.storage, transfer.batch_id)?; batch.status = UnbondBatchStatus::Withdrawn; attrs.push(attr("batch_id", transfer.batch_id.to_string())); attrs.push(attr("unbond_batch_status", "withdrawn")); - UNBOND_BATCHES.save(deps.storage, transfer.batch_id, &batch)?; + unbond_batches_map().save(deps.storage, transfer.batch_id, &batch)?; } } _ => return Err(ContractError::InvalidTransaction {}), @@ -422,13 +468,13 @@ fn execute_tick_staking( let batch_id = FAILED_BATCH_ID .may_load(deps.storage)? .unwrap_or(UNBOND_BATCH_ID.load(deps.storage)?); - let mut unbond = UNBOND_BATCHES.load(deps.storage, batch_id)?; + let mut unbond = unbond_batches_map().load(deps.storage, batch_id)?; if (Timestamp::from_seconds(unbond.created).plus_seconds(config.unbond_batch_switch_time) > env.block.time) && !unbond.unbond_items.is_empty() && !unbond.total_amount.is_zero() { - let (pre_unbonding_balance, _) = get_ica_balance_by_denom( + let (pre_unbonding_balance, _, _) = get_ica_balance_by_denom( deps.as_ref(), &config.puppeteer_contract, &config.remote_denom, @@ -458,9 +504,9 @@ fn execute_tick_staking( funds: info.funds, })); unbond.status = UnbondBatchStatus::UnbondRequested; - UNBOND_BATCHES.save(deps.storage, batch_id, &unbond)?; + unbond_batches_map().save(deps.storage, batch_id, &unbond)?; UNBOND_BATCH_ID.save(deps.storage, &(batch_id + 1))?; - UNBOND_BATCHES.save( + unbond_batches_map().save( deps.storage, batch_id + 1, &new_unbond(env.block.time.seconds()), @@ -486,10 +532,10 @@ fn execute_tick_unbonding( lido_puppeteer_base::msg::Transaction::Undelegate { batch_id, .. } => { LAST_PUPPETEER_RESPONSE.remove(deps.storage); attrs.push(attr("batch_id", batch_id.to_string())); - let mut unbond = UNBOND_BATCHES.load(deps.storage, batch_id)?; + let mut unbond = unbond_batches_map().load(deps.storage, batch_id)?; unbond.status = UnbondBatchStatus::Unbonding; unbond.expected_release = env.block.time.seconds() + config.unbonding_period; - UNBOND_BATCHES.save(deps.storage, batch_id, &unbond)?; + unbond_batches_map().save(deps.storage, batch_id, &unbond)?; FAILED_BATCH_ID.remove(deps.storage); attrs.push(attr("unbonding", "success")); } @@ -499,9 +545,9 @@ fn execute_tick_unbonding( lido_puppeteer_base::msg::ResponseHookMsg::Error(response) => match response.transaction { lido_puppeteer_base::msg::Transaction::Undelegate { batch_id, .. } => { attrs.push(attr("batch_id", batch_id.to_string())); - let mut unbond = UNBOND_BATCHES.load(deps.storage, batch_id)?; + let mut unbond = unbond_batches_map().load(deps.storage, batch_id)?; unbond.status = UnbondBatchStatus::UnbondFailed; - UNBOND_BATCHES.save(deps.storage, batch_id, &unbond)?; + unbond_batches_map().save(deps.storage, batch_id, &unbond)?; FAILED_BATCH_ID.save(deps.storage, &batch_id)?; attrs.push(attr("unbonding", "failed")); } @@ -512,27 +558,6 @@ fn execute_tick_unbonding( Ok(response("execute-tick_unbonding", CONTRACT_NAME, attrs)) } -fn execute_fake_process_batch( - deps: DepsMut, - _env: Env, - _info: MessageInfo, - batch_id: Uint128, - unbonded_amount: Uint128, -) -> ContractResult> { - let mut attrs = vec![attr("action", "fake_process_batch")]; - let mut unbond_batch = UNBOND_BATCHES.load(deps.storage, batch_id.into())?; - unbond_batch.unbonded_amount = Some(unbonded_amount); - unbond_batch.status = UnbondBatchStatus::Unbonded; - unbond_batch.slashing_effect = Some( - Decimal::from_str(&unbonded_amount.to_string())? - / Decimal::from_str(&unbond_batch.expected_amount.to_string())?, - ); - UNBOND_BATCHES.save(deps.storage, batch_id.into(), &unbond_batch)?; - attrs.push(attr("batch_id", batch_id.to_string())); - attrs.push(attr("unbonded_amount", unbonded_amount.to_string())); - Ok(response("execute-fake_process_batch", CONTRACT_NAME, attrs)) -} - fn execute_bond( deps: DepsMut, env: Env, @@ -683,7 +708,7 @@ fn execute_unbond( let config = CONFIG.load(deps.storage)?; let ld_denom = config.ld_denom.ok_or(ContractError::LDDenomIsNotSet {})?; let amount = cw_utils::must_pay(&info, &ld_denom)?; - let mut unbond_batch = UNBOND_BATCHES.load(deps.storage, unbond_batch_id)?; + let mut unbond_batch = unbond_batches_map().load(deps.storage, unbond_batch_id)?; let exchange_rate = query_exchange_rate(deps.as_ref(), env, None)?; attrs.push(attr("exchange_rate", exchange_rate.to_string())); let expected_amount = amount * exchange_rate; @@ -696,7 +721,7 @@ fn execute_unbond( unbond_batch.expected_amount += expected_amount; attrs.push(attr("expected_amount", expected_amount.to_string())); - UNBOND_BATCHES.save(deps.storage, unbond_batch_id, &unbond_batch)?; + unbond_batches_map().save(deps.storage, unbond_batch_id, &unbond_batch)?; let extension = Some(Metadata { description: Some("Withdrawal voucher".into()), name: "LDV voucher".to_string(), @@ -758,7 +783,7 @@ fn get_unbonded_batch(deps: Deps) -> ContractResult( config: &Config, funds: Vec, ) -> ContractResult> { - let (balance, balance_height) = get_ica_balance_by_denom( + let (balance, balance_height, _) = get_ica_balance_by_denom( deps.as_ref(), &config.puppeteer_contract, &config.remote_denom, @@ -874,19 +899,14 @@ fn get_received_puppeteer_response( fn is_unbonding_time_close( deps: Deps, - now: &u64, - safe_period: &u64, + now: u64, + safe_period: u64, ) -> ContractResult { let mut unbond_batch_id = UNBOND_BATCH_ID.load(deps.storage)?; while unbond_batch_id > 0 { - let unbond_batch = UNBOND_BATCHES.load(deps.storage, unbond_batch_id)?; - if unbond_batch.status == UnbondBatchStatus::Unbonding - && (now - unbond_batch.expected_release < *safe_period) - { - return Ok(true); - } - if unbond_batch.status == UnbondBatchStatus::Unbonded { - return Ok(false); + let unbond_batch = unbond_batches_map().load(deps.storage, unbond_batch_id)?; + if unbond_batch.status == UnbondBatchStatus::Unbonding { + return Ok(now - unbond_batch.expected_release < safe_period); } unbond_batch_id -= 1; } @@ -898,8 +918,8 @@ fn get_ica_balance_by_denom( puppeteer_contract: &str, remote_denom: &str, can_be_zero: bool, -) -> ContractResult<(Uint128, u64)> { - let (ica_balances, remote_height): lido_staking_base::msg::puppeteer::BalancesResponse = +) -> ContractResult<(Uint128, u64, u64)> { + let (ica_balances, local_height, local_time): lido_staking_base::msg::puppeteer::BalancesResponse = deps.querier.query_wasm_smart( puppeteer_contract.to_string(), &lido_puppeteer_base::msg::QueryMsg::Extention { @@ -919,12 +939,13 @@ fn get_ica_balance_by_denom( true => balance.unwrap_or(Uint128::zero()), false => balance.ok_or(ContractError::ICABalanceZero {})?, }, - remote_height, + local_height, + local_time.seconds(), )) } -fn new_unbond(now: u64) -> lido_staking_base::state::core::UnbondBatch { - lido_staking_base::state::core::UnbondBatch { +fn new_unbond(now: u64) -> UnbondBatch { + UnbondBatch { total_amount: Uint128::zero(), expected_amount: Uint128::zero(), unbond_items: vec![], diff --git a/contracts/core/src/error.rs b/contracts/core/src/error.rs index 2b975fec..ff461513 100644 --- a/contracts/core/src/error.rs +++ b/contracts/core/src/error.rs @@ -65,7 +65,7 @@ pub enum ContractError { #[error("Non Native rewards denom not found {denom}")] NonNativeRewardsDenomNotFound { denom: String }, - #[error("Puppereer balance is outdated: ICA balance height {ica_height}, puppeteer balance height {puppeteer_height}")] + #[error("Puppeteer balance is outdated: ICA balance height {ica_height}, puppeteer balance height {puppeteer_height}")] PuppeteerBalanceOutdated { ica_height: u64, puppeteer_height: u64, diff --git a/contracts/core/src/tests.rs b/contracts/core/src/tests.rs index 6e574653..3c13f88f 100644 --- a/contracts/core/src/tests.rs +++ b/contracts/core/src/tests.rs @@ -4,7 +4,8 @@ use cosmwasm_std::{ from_json, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, to_json_binary, Coin, ContractResult, CosmosMsg, Decimal, Empty, Order, OwnedDeps, Querier, - QuerierResult, QueryRequest, StdResult, SystemError, SystemResult, Uint128, WasmMsg, WasmQuery, + QuerierResult, QueryRequest, StdResult, SystemError, SystemResult, Timestamp, Uint128, WasmMsg, + WasmQuery, }; use lido_puppeteer_base::msg::QueryMsg as PuppeteerBaseQueryMsg; @@ -72,9 +73,10 @@ impl WasmMockQuerier { amount: Uint128::new(150), }], }, - 10, + 10u64, + Timestamp::from_nanos(20), ); - to_json_binary(&(data.0, data.1)) + to_json_binary(&data) } QueryExtMsg::Balances {} => { let data = ( @@ -84,9 +86,10 @@ impl WasmMockQuerier { amount: Uint128::new(200), }], }, - 10, + 10u64, + Timestamp::from_nanos(20), ); - to_json_binary(&(data.0, data.1)) + to_json_binary(&data) } _ => todo!(), }, diff --git a/contracts/puppeteer/src/contract.rs b/contracts/puppeteer/src/contract.rs index da088730..cbd54d0d 100644 --- a/contracts/puppeteer/src/contract.rs +++ b/contracts/puppeteer/src/contract.rs @@ -16,7 +16,7 @@ use cosmos_sdk_proto::cosmos::{ }; use cosmwasm_std::{ attr, ensure_eq, entry_point, to_json_binary, Addr, CosmosMsg, Deps, Order, Reply, StdError, - SubMsg, Uint128, WasmMsg, + SubMsg, Timestamp, Uint128, WasmMsg, }; use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; @@ -98,6 +98,7 @@ pub fn instantiate( }, }, 0, + Timestamp::default(), ), )?; Puppeteer::default().instantiate(deps, config) @@ -141,16 +142,16 @@ fn query_fees(deps: Deps) -> ContractResult { fn query_delegations(deps: Deps) -> ContractResult { let data = DELEGATIONS_AND_BALANCE.load(deps.storage)?; - to_json_binary(&(data.0.delegations, data.1)).map_err(ContractError::Std) + to_json_binary(&(data.0.delegations, data.1, data.2)).map_err(ContractError::Std) } fn query_balances(deps: Deps) -> ContractResult { let data = DELEGATIONS_AND_BALANCE.load(deps.storage)?; - to_json_binary(&(data.0.balances, data.1)).map_err(ContractError::Std) + to_json_binary(&(data.0.balances, data.1, data.2)).map_err(ContractError::Std) } fn query_non_native_rewards_balances(deps: Deps) -> ContractResult { let data = NON_NATIVE_REWARD_BALANCES.load(deps.storage)?; - to_json_binary(&(data.0, data.1)).map_err(ContractError::Std) + to_json_binary(&(data.0, data.1, data.2)).map_err(ContractError::Std) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/strategy/src/tests.rs b/contracts/strategy/src/tests.rs index 1ad2f6a0..31a7714d 100644 --- a/contracts/strategy/src/tests.rs +++ b/contracts/strategy/src/tests.rs @@ -4,7 +4,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ to_json_binary, Addr, Attribute, Binary, Decimal, Deps, Empty, Env, Event, Response, StdResult, - Uint128, + Timestamp, Uint128, }; use cw_multi_test::{custom_app, App, Contract, ContractWrapper, Executor}; use lido_puppeteer_base::error::ContractError as PuppeteerContractError; @@ -95,7 +95,8 @@ fn puppeteer_query( Delegations { delegations: delegations_amount, }, - 0, + 0u64, + Timestamp::default(), ); Ok(to_json_binary(&delegations)?) } diff --git a/contracts/withdrawal-manager/src/contract.rs b/contracts/withdrawal-manager/src/contract.rs index be2216da..31470e9b 100644 --- a/contracts/withdrawal-manager/src/contract.rs +++ b/contracts/withdrawal-manager/src/contract.rs @@ -149,8 +149,8 @@ fn execute_receive_nft_withdraw( )?; ensure_eq!( unbond_batch.status, - UnbondBatchStatus::Unbonded, - ContractError::BatchIsNotUnbonded {} + UnbondBatchStatus::Withdrawn, + ContractError::BatchIsNotWithdrawn {} ); let slashing_effect = unbond_batch .slashing_effect diff --git a/contracts/withdrawal-manager/src/error.rs b/contracts/withdrawal-manager/src/error.rs index 2864c0c6..638abafe 100644 --- a/contracts/withdrawal-manager/src/error.rs +++ b/contracts/withdrawal-manager/src/error.rs @@ -19,8 +19,8 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, - #[error("Batch is not unbonded yet")] - BatchIsNotUnbonded {}, + #[error("Batch is not withdrawn yet")] + BatchIsNotWithdrawn {}, #[error("Missing unbonded amount in batch")] BatchAmountIsEmpty {}, diff --git a/integration_tests/dockerfiles/neutron-query-relayer/build.sh b/integration_tests/dockerfiles/neutron-query-relayer/build.sh index 6fe39e40..d5ee13a9 100755 --- a/integration_tests/dockerfiles/neutron-query-relayer/build.sh +++ b/integration_tests/dockerfiles/neutron-query-relayer/build.sh @@ -1,7 +1,7 @@ #!/bin/bash DIR="$(dirname $0)" cd $DIR -git clone https://github.com/neutron-org/neutron-query-relayer +git clone -b foxpy/low-submission-margin-period https://github.com/neutron-org/neutron-query-relayer VERSION=$(cat ../../package.json | jq -r '.version') if [[ "$CI" == "true" ]]; then VERSION="_$VERSION" @@ -15,4 +15,4 @@ COMMIT=$(git log -1 --format='%H') ldflags="-X github.com/neutron-org/neutron-query-relayer/internal/app.Version=$GVERSION -X github.com/neutron-org/neutron-query-relayer/internal/app.Commit=$COMMIT" docker build --build-arg LDFLAGS="$ldflags" . -t ${ORG}neutron-query-relayer-test${VERSION} cd .. -rm -rf ./neutron-query-relayer \ No newline at end of file +rm -rf ./neutron-query-relayer diff --git a/integration_tests/package.json b/integration_tests/package.json index 91f95a44..69ac6d37 100644 --- a/integration_tests/package.json +++ b/integration_tests/package.json @@ -1,6 +1,6 @@ { "name": "lido-cosmos-integration-tests", - "version": "1.0.0", + "version": "1.0.1", "main": "vitest", "license": "MIT", "scripts": { @@ -9,7 +9,6 @@ "test:poc-provider-proposals": "vitest --run poc-provider-proposals.test --bail 1", "test:poc-proposal-votes": "vitest --run poc-proposal-votes.test --bail 1", "test:core": "vitest --run core.test.ts --bail 1", - "test:core:fsm": "vitest --run core.fsm --bail 1", "test:pump": "vitest --run pump --bail 1", "test:pump-multi": "vitest --run pump-multi --bail 1", "test:puppeteer": "vitest --run puppeteer.test --bail 1", diff --git a/integration_tests/src/generated/contractLib/lidoCore.ts b/integration_tests/src/generated/contractLib/lidoCore.ts index 7d98d184..f9175325 100644 --- a/integration_tests/src/generated/contractLib/lidoCore.ts +++ b/integration_tests/src/generated/contractLib/lidoCore.ts @@ -1,10 +1,5 @@ -import { - CosmWasmClient, - SigningCosmWasmClient, - ExecuteResult, - InstantiateResult, -} from '@cosmjs/cosmwasm-stargate'; -import { StdFee } from '@cosmjs/amino'; +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult, InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import { StdFee } from "@cosmjs/amino"; /** * A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0 * @@ -14,6 +9,7 @@ export type Decimal = string; export interface InstantiateMsg { base_denom: string; + channel: string; fee?: Decimal | null; fee_address?: string | null; idle_min_interval: number; @@ -37,12 +33,7 @@ export interface InstantiateMsg { * The greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18) */ export type Decimal = string; -export type ContractState = - | 'idle' - | 'claiming' - | 'unbonding' - | 'staking' - | 'transfering'; +export type ContractState = "idle" | "claiming" | "unbonding" | "staking" | "transfering"; /** * A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0 * @@ -174,12 +165,13 @@ export type Transaction = }; export type ArrayOfNonNativeRewardsItem = NonNativeRewardsItem[]; export type UnbondBatchStatus = - | 'new' - | 'unbond_requested' - | 'unbond_failed' - | 'unbonding' - | 'unbonded' - | 'withdrawn'; + | "new" + | "unbond_requested" + | "unbond_failed" + | "unbonding" + | "unbonded" + | "withdrawing" + | "withdrawn"; export type PuppeteerHookArgs = | { success: ResponseHookSuccessMsg; @@ -197,8 +189,8 @@ export type UpdateOwnershipArgs = new_owner: string; }; } - | 'accept_ownership' - | 'renounce_ownership'; + | "accept_ownership" + | "renounce_ownership"; /** * Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future) */ @@ -238,25 +230,14 @@ export type Timestamp2 = Uint64; export type Uint64 = string; export interface LidoCoreSchema { - responses: - | Config - | ContractState - | Decimal1 - | ResponseHookMsg - | ArrayOfNonNativeRewardsItem - | UnbondBatch; + responses: Config | ContractState | Decimal1 | ResponseHookMsg | ArrayOfNonNativeRewardsItem | UnbondBatch; query: UnbondBatchArgs; - execute: - | BondArgs - | UpdateConfigArgs - | UpdateNonNativeRewardsReceiversArgs - | FakeProcessBatchArgs - | PuppeteerHookArgs - | UpdateOwnershipArgs; + execute: BondArgs | UpdateConfigArgs | UpdateNonNativeRewardsReceiversArgs | PuppeteerHookArgs | UpdateOwnershipArgs; [k: string]: unknown; } export interface Config { base_denom: string; + channel: string; fee?: Decimal | null; fee_address?: string | null; idle_min_interval: number; @@ -369,6 +350,7 @@ export interface UpdateConfigArgs { } export interface ConfigOptional { base_denom?: string | null; + channel?: string | null; fee?: Decimal | null; fee_address?: string | null; idle_min_interval?: number | null; @@ -390,13 +372,10 @@ export interface ConfigOptional { export interface UpdateNonNativeRewardsReceiversArgs { items: NonNativeRewardsItem[]; } -export interface FakeProcessBatchArgs { - batch_id: Uint128; - unbonded_amount: Uint128; -} + function isSigningCosmWasmClient( - client: CosmWasmClient | SigningCosmWasmClient, + client: CosmWasmClient | SigningCosmWasmClient ): client is SigningCosmWasmClient { return 'execute' in client; } @@ -404,15 +383,12 @@ function isSigningCosmWasmClient( export class Client { private readonly client: CosmWasmClient | SigningCosmWasmClient; contractAddress: string; - constructor( - client: CosmWasmClient | SigningCosmWasmClient, - contractAddress: string, - ) { + constructor(client: CosmWasmClient | SigningCosmWasmClient, contractAddress: string) { this.client = client; this.contractAddress = contractAddress; } mustBeSigningClient() { - return new Error('This client is not a SigningCosmWasmClient'); + return new Error("This client is not a SigningCosmWasmClient"); } static async instantiate( client: SigningCosmWasmClient, @@ -428,175 +404,50 @@ export class Client { }); return res; } - queryConfig = async (): Promise => - this.client.queryContractSmart(this.contractAddress, { config: {} }); - queryExchangeRate = async (): Promise => - this.client.queryContractSmart(this.contractAddress, { exchange_rate: {} }); - queryUnbondBatch = async (args: UnbondBatchArgs): Promise => - this.client.queryContractSmart(this.contractAddress, { - unbond_batch: args, - }); - queryContractState = async (): Promise => - this.client.queryContractSmart(this.contractAddress, { - contract_state: {}, - }); - queryLastPuppeteerResponse = async (): Promise => - this.client.queryContractSmart(this.contractAddress, { - last_puppeteer_response: {}, - }); - queryNonNativeRewardsReceivers = - async (): Promise => - this.client.queryContractSmart(this.contractAddress, { - non_native_rewards_receivers: {}, - }); - bond = async ( - sender: string, - args: BondArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { bond: args }, - fee || 'auto', - memo, - funds, - ); - }; - unbond = async ( - sender: string, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { unbond: {} }, - fee || 'auto', - memo, - funds, - ); - }; - updateConfig = async ( - sender: string, - args: UpdateConfigArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { update_config: args }, - fee || 'auto', - memo, - funds, - ); - }; - updateNonNativeRewardsReceivers = async ( - sender: string, - args: UpdateNonNativeRewardsReceiversArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { update_non_native_rewards_receivers: args }, - fee || 'auto', - memo, - funds, - ); - }; - fakeProcessBatch = async ( - sender: string, - args: FakeProcessBatchArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { fake_process_batch: args }, - fee || 'auto', - memo, - funds, - ); - }; - tick = async ( - sender: string, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { tick: {} }, - fee || 'auto', - memo, - funds, - ); - }; - puppeteerHook = async ( - sender: string, - args: PuppeteerHookArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { puppeteer_hook: args }, - fee || 'auto', - memo, - funds, - ); - }; - updateOwnership = async ( - sender: string, - args: UpdateOwnershipArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { update_ownership: args }, - fee || 'auto', - memo, - funds, - ); - }; + queryConfig = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { config: {} }); + } + queryExchangeRate = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { exchange_rate: {} }); + } + queryUnbondBatch = async(args: UnbondBatchArgs): Promise => { + return this.client.queryContractSmart(this.contractAddress, { unbond_batch: args }); + } + queryContractState = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { contract_state: {} }); + } + queryLastPuppeteerResponse = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { last_puppeteer_response: {} }); + } + queryNonNativeRewardsReceivers = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { non_native_rewards_receivers: {} }); + } + bond = async(sender:string, args: BondArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { bond: args }, fee || "auto", memo, funds); + } + unbond = async(sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { unbond: {} }, fee || "auto", memo, funds); + } + updateConfig = async(sender:string, args: UpdateConfigArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { update_config: args }, fee || "auto", memo, funds); + } + updateNonNativeRewardsReceivers = async(sender:string, args: UpdateNonNativeRewardsReceiversArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { update_non_native_rewards_receivers: args }, fee || "auto", memo, funds); + } + tick = async(sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { tick: {} }, fee || "auto", memo, funds); + } + puppeteerHook = async(sender:string, args: PuppeteerHookArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { puppeteer_hook: args }, fee || "auto", memo, funds); + } + updateOwnership = async(sender:string, args: UpdateOwnershipArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { update_ownership: args }, fee || "auto", memo, funds); + } } diff --git a/integration_tests/src/generated/contractLib/lidoFactory.ts b/integration_tests/src/generated/contractLib/lidoFactory.ts index f3130d9d..43976611 100644 --- a/integration_tests/src/generated/contractLib/lidoFactory.ts +++ b/integration_tests/src/generated/contractLib/lidoFactory.ts @@ -1,11 +1,6 @@ -import { - CosmWasmClient, - SigningCosmWasmClient, - ExecuteResult, - InstantiateResult, -} from '@cosmjs/cosmwasm-stargate'; -import { StdFee } from '@cosmjs/amino'; -import { Coin } from '@cosmjs/amino'; +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult, InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import { StdFee } from "@cosmjs/amino"; +import { Coin } from "@cosmjs/amino"; export interface InstantiateMsg { code_ids: CodeIds; remote_opts: RemoteOpts; @@ -136,12 +131,7 @@ export type Binary = string; export interface LidoFactorySchema { responses: State; - execute: - | InitArgs - | CallbackArgs - | UpdateConfigArgs - | ProxyArgs - | AdminExecuteArgs; + execute: InitArgs | CallbackArgs | UpdateConfigArgs | ProxyArgs | AdminExecuteArgs; [k: string]: unknown; } export interface State { @@ -169,6 +159,7 @@ export interface CoreParams { } export interface ConfigOptional { base_denom?: string | null; + channel?: string | null; fee?: Decimal | null; fee_address?: string | null; idle_min_interval?: number | null; @@ -214,8 +205,9 @@ export interface AdminExecuteArgs { msg: Binary; } + function isSigningCosmWasmClient( - client: CosmWasmClient | SigningCosmWasmClient, + client: CosmWasmClient | SigningCosmWasmClient ): client is SigningCosmWasmClient { return 'execute' in client; } @@ -223,15 +215,12 @@ function isSigningCosmWasmClient( export class Client { private readonly client: CosmWasmClient | SigningCosmWasmClient; contractAddress: string; - constructor( - client: CosmWasmClient | SigningCosmWasmClient, - contractAddress: string, - ) { + constructor(client: CosmWasmClient | SigningCosmWasmClient, contractAddress: string) { this.client = client; this.contractAddress = contractAddress; } mustBeSigningClient() { - return new Error('This client is not a SigningCosmWasmClient'); + return new Error("This client is not a SigningCosmWasmClient"); } static async instantiate( client: SigningCosmWasmClient, @@ -247,101 +236,27 @@ export class Client { }); return res; } - queryState = async (): Promise => - this.client.queryContractSmart(this.contractAddress, { state: {} }); - init = async ( - sender: string, - args: InitArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { init: args }, - fee || 'auto', - memo, - funds, - ); - }; - callback = async ( - sender: string, - args: CallbackArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { callback: args }, - fee || 'auto', - memo, - funds, - ); - }; - updateConfig = async ( - sender: string, - args: UpdateConfigArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { update_config: args }, - fee || 'auto', - memo, - funds, - ); - }; - proxy = async ( - sender: string, - args: ProxyArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { proxy: args }, - fee || 'auto', - memo, - funds, - ); - }; - adminExecute = async ( - sender: string, - args: AdminExecuteArgs, - fee?: number | StdFee | 'auto', - memo?: string, - funds?: Coin[], - ): Promise => { - if (!isSigningCosmWasmClient(this.client)) { - throw this.mustBeSigningClient(); - } - return this.client.execute( - sender, - this.contractAddress, - { admin_execute: args }, - fee || 'auto', - memo, - funds, - ); - }; + queryState = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { state: {} }); + } + init = async(sender:string, args: InitArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { init: args }, fee || "auto", memo, funds); + } + callback = async(sender:string, args: CallbackArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { callback: args }, fee || "auto", memo, funds); + } + updateConfig = async(sender:string, args: UpdateConfigArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { update_config: args }, fee || "auto", memo, funds); + } + proxy = async(sender:string, args: ProxyArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { proxy: args }, fee || "auto", memo, funds); + } + adminExecute = async(sender:string, args: AdminExecuteArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { admin_execute: args }, fee || "auto", memo, funds); + } } diff --git a/integration_tests/src/testSuite.ts b/integration_tests/src/testSuite.ts index 5b03c526..7b78e7f6 100644 --- a/integration_tests/src/testSuite.ts +++ b/integration_tests/src/testSuite.ts @@ -4,7 +4,6 @@ import { StargateClient } from '@cosmjs/stargate'; import { Client as NeutronClient } from '@neutron-org/client-ts'; import { waitFor } from './helpers/waitFor'; import { sleep } from './helpers/sleep'; -import child_process from 'child_process'; const packageJSON = require(`${__dirname}/../package.json`); const VERSION = (process.env.CI ? '_' : ':') + packageJSON.version; const ORG = process.env.CI ? 'neutronorg/lionco-contracts:' : ''; @@ -68,6 +67,7 @@ const networkConfigs = { 'app_state.slashing.params.downtime_jail_duration': '10s', 'app_state.slashing.params.signed_blocks_window': '10', 'app_state.staking.params.validator_bond_factor': '10', + 'app_state.staking.params.unbonding_time': '1814400s', 'app_state.mint.minter.inflation': '0.9', 'app_state.mint.params.inflation_max': '0.95', 'app_state.mint.params.inflation_min': '0.5', @@ -131,10 +131,12 @@ const relayersConfig = { balance: '1000000000', binary: 'hermes', config: { + 'chains.0.gas_multiplier': 1.2, 'chains.0.trusting_period': '112h0m0s', 'chains.0.unbonding_period': '336h0m0s', 'chains.1.gas_multiplier': 1.2, - 'chains.0.gas_multiplier': 1.2, + 'chains.1.trusting_period': '168h0m0s', + 'chains.1.unbonding_period': '504h0m0s', }, image: `${ORG}hermes-test${VERSION}`, log_level: 'trace', @@ -231,7 +233,15 @@ export const setupPark = async ( networks: string[] = [], needHermes = false, needNeutronRelayer = false, + needShortUnbondingPeriod = false, ): Promise => { + if (needShortUnbondingPeriod) { + networkConfigs['gaia'].genesis_opts[ + 'app_state.staking.params.unbonding_time' + ] = '360s'; + relayersConfig['hermes'].config['chains.1.trusting_period'] = '2m0s'; + relayersConfig['hermes'].config['chains.1.unbonding_period'] = '6m0s'; + } const wallets = await generateWallets(); const config: CosmoparkConfig = { context, @@ -290,11 +300,6 @@ export const setupPark = async ( `127.0.0.1:${instance.ports['neutron'].rest}`, `127.0.0.1:${instance.ports['neutron'].rpc}`, ).catch((e) => { - console.log( - child_process - .execSync('docker logs corefsm-relayer_hermes0-1') - .toString(), - ); console.log(`Failed to await neutron channels: ${e}`); throw e; }); diff --git a/integration_tests/src/testcases/auto-withdrawer.test.ts b/integration_tests/src/testcases/auto-withdrawer.test.ts index baa0b1ce..b6089336 100644 --- a/integration_tests/src/testcases/auto-withdrawer.test.ts +++ b/integration_tests/src/testcases/auto-withdrawer.test.ts @@ -3,6 +3,9 @@ import { LidoAutoWithdrawer, LidoCore, LidoFactory, + LidoPump, + LidoPuppeteer, + LidoStrategy, LidoWithdrawalManager, LidoWithdrawalVoucher, } from '../generated/contractLib'; @@ -19,15 +22,28 @@ import { join } from 'path'; import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; import { Client as NeutronClient } from '@neutron-org/client-ts'; -import { AccountData, DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; +import { + AccountData, + Coin, + DirectSecp256k1HdWallet, +} from '@cosmjs/proto-signing'; import { GasPrice } from '@cosmjs/stargate'; import { setupPark } from '../testSuite'; import fs from 'fs'; import Cosmopark from '@neutron-org/cosmopark'; import { waitFor } from '../helpers/waitFor'; +import { + ResponseHookMsg, + ResponseHookSuccessMsg, +} from '../generated/contractLib/lidoCore'; +import { stringToPath } from '@cosmjs/crypto'; +import { sleep } from '../helpers/sleep'; const LidoFactoryClass = LidoFactory.Client; const LidoCoreClass = LidoCore.Client; +const LidoPumpClass = LidoPump.Client; +const LidoPuppeteerClass = LidoPuppeteer.Client; +const LidoStrategyClass = LidoStrategy.Client; const LidoWithdrawalVoucherClass = LidoWithdrawalVoucher.Client; const LidoWithdrawalManagerClass = LidoWithdrawalManager.Client; const LidoAutoWithdrawerClass = LidoAutoWithdrawer.Client; @@ -38,8 +54,12 @@ describe('Auto withdrawer', () => { contractAddress?: string; wallet?: DirectSecp256k1HdWallet; gaiaWallet?: DirectSecp256k1HdWallet; - contractClient?: InstanceType; + gaiaWallet2?: DirectSecp256k1HdWallet; + factoryContractClient?: InstanceType; coreContractClient?: InstanceType; + strategyContractClient?: InstanceType; + pumpContractClient?: InstanceType; + puppeteerContractClient?: InstanceType; withdrawalVoucherContractClient?: InstanceType< typeof LidoWithdrawalVoucherClass >; @@ -52,6 +72,7 @@ describe('Auto withdrawer', () => { client?: SigningCosmWasmClient; gaiaClient?: SigningStargateClient; gaiaUserAddress?: string; + gaiaUserAddress2?: string; gaiaQueryClient?: QueryClient & StakingExtension & BankExtension; neutronClient?: InstanceType; neutronUserAddress?: string; @@ -74,12 +95,16 @@ describe('Auto withdrawer', () => { tokenContractAddress?: string; neutronIBCDenom?: string; ldDenom?: string; - } = { - codeIds: {}, - }; + } = { codeIds: {} }; beforeAll(async () => { - context.park = await setupPark('autowithdrawer', ['neutron', 'gaia'], true); + context.park = await setupPark( + 'autowithdrawer', + ['neutron', 'gaia'], + true, + true, + true, + ); context.wallet = await DirectSecp256k1HdWallet.fromMnemonic( context.park.config.wallets.demowallet1.mnemonic, { @@ -92,6 +117,12 @@ describe('Auto withdrawer', () => { prefix: 'cosmos', }, ); + context.gaiaWallet2 = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.wallets.demo1.mnemonic, + { + prefix: 'cosmos', + }, + ); context.account = (await context.wallet.getAccounts())[0]; context.neutronClient = new NeutronClient({ apiURL: `http://127.0.0.1:${context.park.ports.neutron.rest}`, @@ -120,7 +151,6 @@ describe('Auto withdrawer', () => { setupStakingExtension, setupBankExtension, ); - const secondWallet = await DirectSecp256k1HdWallet.fromMnemonic( context.park.config.wallets.demo2.mnemonic, { @@ -138,6 +168,7 @@ describe('Auto withdrawer', () => { it('instantiate', async () => { const { client, account } = context; + context.codeIds = {}; { const res = await client.upload( @@ -234,6 +265,7 @@ describe('Auto withdrawer', () => { expect(res.codeId).toBeGreaterThan(0); context.codeIds.rewardsManager = res.codeId; } + const res = await client.upload( account.address, fs.readFileSync(join(__dirname, '../../../artifacts/lido_factory.wasm')), @@ -281,16 +313,39 @@ describe('Auto withdrawer', () => { ); expect(instantiateRes.contractAddress).toHaveLength(66); context.contractAddress = instantiateRes.contractAddress; - context.contractClient = new LidoFactory.Client( + context.factoryContractClient = new LidoFactory.Client( client, context.contractAddress, ); context.gaiaUserAddress = ( await context.gaiaWallet.getAccounts() )[0].address; + context.gaiaUserAddress2 = ( + await context.gaiaWallet2.getAccounts() + )[0].address; context.neutronUserAddress = ( await context.wallet.getAccounts() )[0].address; + { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.master_mnemonic, + { + prefix: 'cosmosvaloper', + hdPaths: [stringToPath("m/44'/118'/1'/0/0") as any], + }, + ); + context.validatorAddress = (await wallet.getAccounts())[0].address; + } + { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.master_mnemonic, + { + prefix: 'cosmosvaloper', + hdPaths: [stringToPath("m/44'/118'/2'/0/0") as any], + }, + ); + context.secondValidatorAddress = (await wallet.getAccounts())[0].address; + } }); it('transfer tokens to neutron', async () => { @@ -330,23 +385,60 @@ describe('Auto withdrawer', () => { }); expect(context.neutronIBCDenom).toBeTruthy(); }); + it('init', async () => { - const { contractClient, neutronUserAddress, neutronIBCDenom } = context; - { - const res = await contractClient.init(context.neutronUserAddress, { - base_denom: context.neutronIBCDenom, - core_params: { - idle_min_interval: 1, - puppeteer_timeout: 60, - unbond_batch_switch_time: 6000, - unbonding_safe_period: 10, - unbonding_period: 60, - channel: 'channel-0', - }, - }); - expect(res.transactionHash).toHaveLength(64); - } + const { factoryContractClient: contractClient } = context; + const res = await contractClient.init(context.neutronUserAddress, { + base_denom: context.neutronIBCDenom, + core_params: { + idle_min_interval: 60, + puppeteer_timeout: 60, + unbond_batch_switch_time: 240, + unbonding_safe_period: 10, + unbonding_period: 360, + channel: 'channel-0', + }, + }); + expect(res.transactionHash).toHaveLength(64); + }); + + it('query factory state', async () => { + const { factoryContractClient: contractClient, neutronClient } = context; const res = await contractClient.queryState(); + expect(res).toBeTruthy(); + const tokenContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.token_contract, + ); + expect(tokenContractInfo.data.contract_info.label).toBe( + 'LIDO-staking-token', + ); + const coreContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.core_contract, + ); + expect(coreContractInfo.data.contract_info.label).toBe('LIDO-staking-core'); + const withdrawalVoucherContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.withdrawal_voucher_contract, + ); + expect(withdrawalVoucherContractInfo.data.contract_info.label).toBe( + 'LIDO-staking-withdrawal-voucher', + ); + const withdrawalManagerContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.withdrawal_manager_contract, + ); + expect(withdrawalManagerContractInfo.data.contract_info.label).toBe( + 'LIDO-staking-withdrawal-manager', + ); + const puppeteerContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.puppeteer_contract, + ); + expect(puppeteerContractInfo.data.contract_info.label).toBe( + 'LIDO-staking-puppeteer', + ); context.coreContractClient = new LidoCore.Client( context.client, res.core_contract, @@ -359,12 +451,106 @@ describe('Auto withdrawer', () => { context.client, res.withdrawal_manager_contract, ); + context.strategyContractClient = new LidoStrategy.Client( + context.client, + res.strategy_contract, + ); context.tokenContractAddress = res.token_contract; - context.exchangeRate = parseFloat( - await context.coreContractClient.queryExchangeRate(), + context.puppeteerContractClient = new LidoPuppeteer.Client( + context.client, + res.puppeteer_contract, ); context.ldDenom = `factory/${context.tokenContractAddress}/lido`; + }); + + it('set fees for puppeteer', async () => { + const { neutronUserAddress, factoryContractClient: contractClient } = + context; + const res = await contractClient.updateConfig(neutronUserAddress, { + puppeteer_fees: { + timeout_fee: '10000', + ack_fee: '10000', + recv_fee: '0', + register_fee: '1000000', + }, + }); + expect(res.transactionHash).toHaveLength(64); + }); + + it('register ICA', async () => { + const { puppeteerContractClient, neutronUserAddress } = context; + const res = await puppeteerContractClient.registerICA( + neutronUserAddress, + 1.5, + undefined, + [{ amount: '1000000', denom: 'untrn' }], + ); + expect(res.transactionHash).toHaveLength(64); + let ica = ''; + await waitFor(async () => { + const res = await puppeteerContractClient.queryIca(); + switch (res) { + case 'none': + case 'in_progress': + case 'timeout': + return false; + default: + ica = res.registered.ica_address; + return true; + } + }, 100_000); + expect(ica).toHaveLength(65); + expect(ica.startsWith('cosmos')).toBeTruthy(); + context.icaAddress = ica; + }); + it('query exchange rate', async () => { + const { coreContractClient } = context; + context.exchangeRate = parseFloat( + await coreContractClient.queryExchangeRate(), + ); + expect(context.exchangeRate).toEqual(1); + }); + + it('add validators into validators set', async () => { + const { + neutronUserAddress, + factoryContractClient, + validatorAddress, + secondValidatorAddress, + } = context; + const res = await factoryContractClient.proxy( + neutronUserAddress, + { + validator_set: { + update_validators: { + validators: [ + { + valoper_address: validatorAddress, + weight: 1, + }, + { + valoper_address: secondValidatorAddress, + weight: 1, + }, + ], + }, + }, + }, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + }); + it('setup auto withdrawer', async () => { + const { client, account, ldDenom, neutronUserAddress, neutronIBCDenom } = + context; { const res = await context.coreContractClient.bond( neutronUserAddress, @@ -373,7 +559,7 @@ describe('Auto withdrawer', () => { undefined, [ { - amount: '500000', + amount: '1000000', denom: neutronIBCDenom, }, ], @@ -387,16 +573,13 @@ describe('Auto withdrawer', () => { undefined, [ { - amount: Math.floor(400_000 / context.exchangeRate).toString(), + amount: Math.floor(500_000 / context.exchangeRate).toString(), denom: context.ldDenom, }, ], ); expect(res.transactionHash).toHaveLength(64); } - }); - it('setup auto withdrawer', async () => { - const { client, account, ldDenom } = context; const res = await client.upload( account.address, fs.readFileSync( @@ -427,8 +610,6 @@ describe('Auto withdrawer', () => { instantiateRes.contractAddress, ); }); - - // TODO: test deposit it('bond with ld assets', async () => { const { neutronUserAddress, ldDenom, autoWithdrawerContractClient } = context; @@ -441,7 +622,7 @@ describe('Auto withdrawer', () => { undefined, [ { - amount: String(2000), + amount: String(20000), denom: ldDenom, }, { @@ -471,7 +652,6 @@ describe('Auto withdrawer', () => { next_page_key: null, }); }); - it('unbond', async () => { const { neutronUserAddress, @@ -502,7 +682,6 @@ describe('Auto withdrawer', () => { next_page_key: null, }); }); - it('bond with NFT', async () => { const { neutronUserAddress, @@ -558,78 +737,658 @@ describe('Auto withdrawer', () => { }); }); - // TODO: figure out how to sign this tx from neutronSecondUserAccount - it('try to withdraw before unbonding period is over', async () => { - const { neutronUserAddress, autoWithdrawerContractClient } = context; - - await expect( - autoWithdrawerContractClient.withdraw( - neutronUserAddress, - { - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, - }, - 1.6, - undefined, - [], - ), - ).rejects.toThrowError(/is not unbonded yet/); - }); - - it('fake unbonding period', async () => { - const { neutronUserAddress, neutronIBCDenom } = context; - await context.coreContractClient.fakeProcessBatch(neutronUserAddress, { - batch_id: '0', - unbonded_amount: '499999', + describe('state machine', () => { + const ica: { balance?: number } = {}; + describe('prepare', () => { + it('get ICA balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + ica.balance = parseInt(res.amount); + expect(ica.balance).toEqual(0); + }); + it('deploy pump', async () => { + const { client, account, neutronUserAddress } = context; + const resUpload = await client.upload( + account.address, + fs.readFileSync(join(__dirname, '../../../artifacts/lido_pump.wasm')), + 1.5, + ); + expect(resUpload.codeId).toBeGreaterThan(0); + const { codeId } = resUpload; + const res = await LidoPump.Client.instantiate( + client, + neutronUserAddress, + codeId, + { + connection_id: 'connection-0', + ibc_fees: { + timeout_fee: '10000', + ack_fee: '10000', + recv_fee: '0', + register_fee: '1000000', + }, + local_denom: 'untrn', + timeout: { + local: 60, + remote: 60, + }, + dest_address: + context.withdrawalManagerContractClient.contractAddress, + dest_port: 'transfer', + dest_channel: 'channel-0', + refundee: neutronUserAddress, + owner: account.address, + }, + 'Lido-staking-pump', + [], + 1.5, + ); + expect(res.contractAddress).toHaveLength(66); + context.pumpContractClient = new LidoPump.Client( + client, + res.contractAddress, + ); + await context.pumpContractClient.registerICA( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + let ica = ''; + await waitFor(async () => { + const res = await context.pumpContractClient.queryIca(); + switch (res) { + case 'none': + case 'in_progress': + case 'timeout': + return false; + default: + ica = res.registered.ica_address; + return true; + } + }, 50_000); + expect(ica).toHaveLength(65); + expect(ica.startsWith('cosmos')).toBeTruthy(); + const resFactory = await context.factoryContractClient.updateConfig( + neutronUserAddress, + { + core: { + pump_address: ica, + }, + }, + ); + expect(resFactory.transactionHash).toHaveLength(64); + }); + it('get machine state', async () => { + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + }); + describe('first cycle', () => { + it('tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('transfering'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('get ICA increased balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + const balance = parseInt(res.amount); + expect(balance - 1000000).toEqual(ica.balance); + ica.balance = balance; + }); + it('wait for balances to come', async () => { + let res; + await waitFor(async () => { + try { + res = await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + }); + } catch (e) { + // + } + return res && res[0].coins.length !== 0; + }, 100_000); + }); + it('second tick goes to staking', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('query strategy contract to see delegations', async () => { + await waitFor(async () => { + try { + await context.strategyContractClient.queryCalcWithdraw({ + withdraw: '500000', + }); + return true; + } catch (e) { + return false; + } + }, 100_000); + }); + it('third tick goes to unbonding', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('unbonding'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('next tick goes to idle', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + }); + describe('second cycle', () => { + let balance = 0; + it('get ICA balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + balance = parseInt(res.amount); + }); + it('wait for 30 seconds', async () => { + await sleep(30_000); + }); + it('idle tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('claiming'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('get ICA balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + const newBalance = parseInt(res.amount); + expect(newBalance).toBeGreaterThan(balance); + }); + it('wait for balance to update', async () => { + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 30_000); + }); + it('next tick goes to staking', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('next tick goes to idle', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + }); + describe('third cycle', () => { + let remoteNonNativeDenoms: string[] = []; + it('generate two new tokenfactory tokens and send them to the remote zone', async () => { + const { neutronUserAddress } = context; + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory create-denom test1 --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory create-denom test2 --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + const denoms = + await context.neutronClient.OsmosisTokenfactoryV1Beta1.query.queryDenomsFromCreator( + neutronUserAddress, + ); + expect(denoms.data.denoms.length).toEqual(2); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory mint 1000000${denoms.data.denoms[0]} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory mint 1000000${denoms.data.denoms[1]} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronUserAddress, + ); + const tokenFactoryDenoms = balances.data.balances.filter((b) => + b.denom.startsWith('factory/'), + ); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx ibc-transfer transfer transfer channel-0 ${context.icaAddress} 66666${tokenFactoryDenoms[0].denom} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx ibc-transfer transfer transfer channel-0 ${context.icaAddress} 2222${tokenFactoryDenoms[1].denom} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + }); + it('wait for balances to come', async () => { + let res: readonly Coin[] = []; + await waitFor(async () => { + res = await context.gaiaClient.getAllBalances(context.icaAddress); + return ( + res.some((b) => b.amount === '66666') && + res.some((b) => b.amount === '2222') + ); + }, 30_000); + remoteNonNativeDenoms = [ + res.find((b) => b.amount === '66666').denom, + res.find((b) => b.amount === '2222').denom, + ]; + }); + it('setup non-native receivers', async () => { + const { factoryContractClient, neutronUserAddress } = context; + const res = await factoryContractClient.proxy( + neutronUserAddress, + { + core: { + update_non_native_rewards_receivers: { + items: remoteNonNativeDenoms.map((denom) => ({ + denom, + address: context.gaiaUserAddress, + min_amount: '10000', + fee: '0.1', + fee_address: context.gaiaUserAddress2, + })), + }, + }, + }, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + }); + it('update idle interval', async () => { + const { factoryContractClient, neutronUserAddress } = context; + const res = await factoryContractClient.updateConfig( + neutronUserAddress, + { + core: { + idle_min_interval: 10000, + }, + }, + ); + expect(res.transactionHash).toHaveLength(64); + }); + it('wait for non-native balances to come', async () => { + await waitFor(async () => { + try { + const res: any = + await context.puppeteerContractClient.queryExtention({ + msg: { + non_native_rewards_balances: {}, + }, + }); + return res[0].coins.length == 2; + } catch (e) { + // + } + }); + }); + it('tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + it('wait for the response from puppeteer', async () => { + let response: ResponseHookMsg; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 30_000); + expect(response).toBeTruthy(); + expect(response).toHaveProperty('success'); + }); + it('check balances', async () => { + const { gaiaClient } = context; + const receiverBalance = await gaiaClient.getBalance( + context.gaiaUserAddress, + remoteNonNativeDenoms[0], + ); + expect(receiverBalance.amount).toEqual('60000'); + const feeBalance = await gaiaClient.getBalance( + context.gaiaUserAddress2, + remoteNonNativeDenoms[0], + ); + expect(feeBalance.amount).toEqual('6666'); + // this one is still on ICA as amount is below min_amount + const icaBalance = await gaiaClient.getBalance( + context.icaAddress, + remoteNonNativeDenoms[1], + ); + expect(icaBalance.amount).toEqual('2222'); + }); + it('wait for balances to update', async () => { + await waitFor(async () => { + const res: any = await context.puppeteerContractClient.queryExtention( + { + msg: { + non_native_rewards_balances: {}, + }, + }, + ); + return res[0].coins.length === 1; + }); + }, 30_000); }); - await context.client.sendTokens( - neutronUserAddress, - context.withdrawalManagerContractClient.contractAddress, - [{ amount: '500000', denom: neutronIBCDenom }], - 1.6, - undefined, - ); - }); - it('withdraw', async () => { - const { - neutronUserAddress, - neutronClient, - neutronIBCDenom, - autoWithdrawerContractClient, - } = context; + describe('fourth cycle', () => { + let previousResponse: ResponseHookSuccessMsg; - const balanceBefore = parseInt( - ( - await neutronClient.CosmosBankV1Beta1.query.queryBalance( + it('update idle interval', async () => { + const { factoryContractClient, neutronUserAddress } = context; + const res = await factoryContractClient.updateConfig( neutronUserAddress, - { denom: neutronIBCDenom }, - ) - ).data.balance.amount, - ); + { + core: { + idle_min_interval: 10, + }, + }, + ); + expect(res.transactionHash).toHaveLength(64); + await sleep(10 * 1000); + }); + it('wait until unbonding period is finished', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + const currentTime = Math.floor(Date.now() / 1000); + if (batchInfo.expected_release > currentTime) { + const diffMs = (batchInfo.expected_release - currentTime + 1) * 1000; + await sleep(diffMs); + } + }); + it('wait until fresh ICA balance is delivered', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + await waitFor(async () => { + const icaTs = Math.floor( + ( + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any + )[2] / 1e9, + ); + return icaTs > batchInfo.expected_release; + }, 50_000); + }); + it('tick', async () => { + const { coreContractClient, neutronUserAddress } = context; + previousResponse = ( + (await coreContractClient.queryLastPuppeteerResponse()) as { + success: ResponseHookSuccessMsg; + } + ).success; + await coreContractClient.tick(neutronUserAddress, 1.5, undefined, []); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('claiming'); + }); + it('wait for the response from puppeteer', async () => { + let response: ResponseHookMsg; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return ( + (response as { success: ResponseHookSuccessMsg }).success + .request_id > previousResponse.request_id + ); + }, 30_000); + }); + it('wait for balance to update', async () => { + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 30_000); + }); + it('tick', async () => { + const { coreContractClient, neutronUserAddress } = context; + await coreContractClient.tick(neutronUserAddress, 1.5, undefined, []); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + }); + it('fund withdrawal manager', async () => { + const { pumpContractClient, neutronUserAddress } = context; + await pumpContractClient.push( + neutronUserAddress, + { + coins: [{ amount: '500000', denom: 'stake' }], + }, + 1.5, + undefined, + [{ amount: '20000', denom: 'untrn' }], + ); + await waitFor(async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.withdrawalManagerContractClient.contractAddress, + ); + return balances.data.balances.length > 0; + }); + }); + it('withdraw', async () => { + const { + neutronUserAddress, + neutronClient, + neutronIBCDenom, + autoWithdrawerContractClient, + } = context; - const res = await autoWithdrawerContractClient.withdraw( - neutronUserAddress, - { - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, - }, - 1.6, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); + const balanceBefore = parseInt( + ( + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronUserAddress, + { denom: neutronIBCDenom }, + ) + ).data.balance.amount, + ); - const balance = await neutronClient.CosmosBankV1Beta1.query.queryBalance( - neutronUserAddress, - { denom: neutronIBCDenom }, - ); - expect(parseInt(balance.data.balance.amount) - balanceBefore).toBe(2000); + const res = await autoWithdrawerContractClient.withdraw( + neutronUserAddress, + { + token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + }, + 1.6, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); - const bondings = await autoWithdrawerContractClient.queryBondings({ - user: neutronUserAddress, - }); - expect(bondings).toEqual({ - bondings: [], - next_page_key: null, + const balance = + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronUserAddress, + { denom: neutronIBCDenom }, + ); + expect(parseInt(balance.data.balance.amount) - balanceBefore).toBe( + 20000, + ); + + const bondings = await autoWithdrawerContractClient.queryBondings({ + user: neutronUserAddress, + }); + expect(bondings).toEqual({ + bondings: [], + next_page_key: null, + }); + }); }); }); }); diff --git a/integration_tests/src/testcases/core.fsm.test.ts b/integration_tests/src/testcases/core.fsm.test.ts deleted file mode 100644 index d34ac7bb..00000000 --- a/integration_tests/src/testcases/core.fsm.test.ts +++ /dev/null @@ -1,1209 +0,0 @@ -import { describe, expect, it, beforeAll, afterAll } from 'vitest'; -import { - LidoCore, - LidoFactory, - LidoPump, - LidoPuppeteer, - LidoStrategy, - LidoWithdrawalManager, - LidoWithdrawalVoucher, -} from '../generated/contractLib'; -import { - QueryClient, - StakingExtension, - BankExtension, - setupStakingExtension, - setupBankExtension, - SigningStargateClient, -} from '@cosmjs/stargate'; -import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx'; -import { join } from 'path'; -import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; -import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; -import { Client as NeutronClient } from '@neutron-org/client-ts'; -import { - AccountData, - Coin, - DirectSecp256k1HdWallet, -} from '@cosmjs/proto-signing'; -import { GasPrice } from '@cosmjs/stargate'; -import { awaitBlocks, setupPark } from '../testSuite'; -import fs from 'fs'; -import Cosmopark from '@neutron-org/cosmopark'; -import { waitFor } from '../helpers/waitFor'; -import { - ResponseHookMsg, - UnbondBatch, -} from '../generated/contractLib/lidoCore'; -import { stringToPath } from '@cosmjs/crypto'; -import { sleep } from '../helpers/sleep'; - -const LidoFactoryClass = LidoFactory.Client; -const LidoCoreClass = LidoCore.Client; -const LidoPumpClass = LidoPump.Client; -const LidoPuppeteerClass = LidoPuppeteer.Client; -const LidoStrategyClass = LidoStrategy.Client; -const LidoWithdrawalVoucherClass = LidoWithdrawalVoucher.Client; -const LidoWithdrawalManagerClass = LidoWithdrawalManager.Client; - -describe('Core', () => { - const context: { - park?: Cosmopark; - contractAddress?: string; - wallet?: DirectSecp256k1HdWallet; - gaiaWallet?: DirectSecp256k1HdWallet; - gaiaWallet2?: DirectSecp256k1HdWallet; - factoryContractClient?: InstanceType; - coreContractClient?: InstanceType; - strategyContractClient?: InstanceType; - pumpContractClient?: InstanceType; - puppeteerContractClient?: InstanceType; - withdrawalVoucherContractClient?: InstanceType< - typeof LidoWithdrawalVoucherClass - >; - withdrawalManagerContractClient?: InstanceType< - typeof LidoWithdrawalManagerClass - >; - account?: AccountData; - icaAddress?: string; - client?: SigningCosmWasmClient; - gaiaClient?: SigningStargateClient; - gaiaUserAddress?: string; - gaiaUserAddress2?: string; - gaiaQueryClient?: QueryClient & StakingExtension & BankExtension; - neutronClient?: InstanceType; - neutronUserAddress?: string; - neutronSecondUserAddress?: string; - validatorAddress?: string; - secondValidatorAddress?: string; - tokenizedDenomOnNeutron?: string; - codeIds: { - core?: number; - token?: number; - withdrawalVoucher?: number; - withdrawalManager?: number; - strategy?: number; - puppeteer?: number; - validatorsSet?: number; - distribution?: number; - rewardsManager?: number; - }; - exchangeRate?: number; - tokenContractAddress?: string; - neutronIBCDenom?: string; - ldDenom?: string; - } = { codeIds: {} }; - - beforeAll(async () => { - context.park = await setupPark('corefsm', ['neutron', 'gaia'], true, true); - context.wallet = await DirectSecp256k1HdWallet.fromMnemonic( - context.park.config.wallets.demowallet1.mnemonic, - { - prefix: 'neutron', - }, - ); - context.gaiaWallet = await DirectSecp256k1HdWallet.fromMnemonic( - context.park.config.wallets.demowallet1.mnemonic, - { - prefix: 'cosmos', - }, - ); - context.gaiaWallet2 = await DirectSecp256k1HdWallet.fromMnemonic( - context.park.config.wallets.demo1.mnemonic, - { - prefix: 'cosmos', - }, - ); - context.account = (await context.wallet.getAccounts())[0]; - context.neutronClient = new NeutronClient({ - apiURL: `http://127.0.0.1:${context.park.ports.neutron.rest}`, - rpcURL: `127.0.0.1:${context.park.ports.neutron.rpc}`, - prefix: 'neutron', - }); - context.client = await SigningCosmWasmClient.connectWithSigner( - `http://127.0.0.1:${context.park.ports.neutron.rpc}`, - context.wallet, - { - gasPrice: GasPrice.fromString('0.025untrn'), - }, - ); - context.gaiaClient = await SigningStargateClient.connectWithSigner( - `http://127.0.0.1:${context.park.ports.gaia.rpc}`, - context.gaiaWallet, - { - gasPrice: GasPrice.fromString('0.025stake'), - }, - ); - const tmClient = await Tendermint34Client.connect( - `http://127.0.0.1:${context.park.ports.gaia.rpc}`, - ); - context.gaiaQueryClient = QueryClient.withExtensions( - tmClient, - setupStakingExtension, - setupBankExtension, - ); - const secondWallet = await DirectSecp256k1HdWallet.fromMnemonic( - context.park.config.wallets.demo2.mnemonic, - { - prefix: 'neutron', - }, - ); - context.neutronSecondUserAddress = ( - await secondWallet.getAccounts() - )[0].address; - }); - - afterAll(async () => { - await context.park.stop(); - }); - - it('instantiate', async () => { - const { client, account } = context; - context.codeIds = {}; - - { - const res = await client.upload( - account.address, - fs.readFileSync(join(__dirname, '../../../artifacts/lido_core.wasm')), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.core = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync(join(__dirname, '../../../artifacts/lido_token.wasm')), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.token = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_withdrawal_voucher.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.withdrawalVoucher = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_withdrawal_manager.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.withdrawalManager = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_strategy.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.strategy = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_distribution.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.distribution = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_validators_set.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.validatorsSet = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_puppeteer.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.puppeteer = res.codeId; - } - { - const res = await client.upload( - account.address, - fs.readFileSync( - join(__dirname, '../../../artifacts/lido_rewards_manager.wasm'), - ), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - context.codeIds.rewardsManager = res.codeId; - } - - const res = await client.upload( - account.address, - fs.readFileSync(join(__dirname, '../../../artifacts/lido_factory.wasm')), - 1.5, - ); - expect(res.codeId).toBeGreaterThan(0); - const instantiateRes = await LidoFactory.Client.instantiate( - client, - account.address, - res.codeId, - { - code_ids: { - core_code_id: context.codeIds.core, - token_code_id: context.codeIds.token, - withdrawal_voucher_code_id: context.codeIds.withdrawalVoucher, - withdrawal_manager_code_id: context.codeIds.withdrawalManager, - strategy_code_id: context.codeIds.strategy, - distribution_code_id: context.codeIds.distribution, - validators_set_code_id: context.codeIds.validatorsSet, - puppeteer_code_id: context.codeIds.puppeteer, - rewards_manager_code_id: context.codeIds.rewardsManager, - }, - remote_opts: { - connection_id: 'connection-0', - transfer_channel_id: 'channel-0', - port_id: 'transfer', - denom: 'stake', - update_period: 2, - }, - salt: 'salt', - subdenom: 'lido', - token_metadata: { - description: 'Lido token', - display: 'lido', - exponent: 6, - name: 'Lido liquid staking token', - symbol: 'LIDO', - uri: null, - uri_hash: null, - }, - }, - 'Lido-staking-factory', - [], - 'auto', - ); - expect(instantiateRes.contractAddress).toHaveLength(66); - context.contractAddress = instantiateRes.contractAddress; - context.factoryContractClient = new LidoFactory.Client( - client, - context.contractAddress, - ); - context.gaiaUserAddress = ( - await context.gaiaWallet.getAccounts() - )[0].address; - context.gaiaUserAddress2 = ( - await context.gaiaWallet2.getAccounts() - )[0].address; - context.neutronUserAddress = ( - await context.wallet.getAccounts() - )[0].address; - { - const wallet = await DirectSecp256k1HdWallet.fromMnemonic( - context.park.config.master_mnemonic, - { - prefix: 'cosmosvaloper', - hdPaths: [stringToPath("m/44'/118'/1'/0/0") as any], - }, - ); - context.validatorAddress = (await wallet.getAccounts())[0].address; - } - { - const wallet = await DirectSecp256k1HdWallet.fromMnemonic( - context.park.config.master_mnemonic, - { - prefix: 'cosmosvaloper', - hdPaths: [stringToPath("m/44'/118'/2'/0/0") as any], - }, - ); - context.secondValidatorAddress = (await wallet.getAccounts())[0].address; - } - }); - - it('transfer tokens to neutron', async () => { - const { gaiaClient, gaiaUserAddress, neutronUserAddress, neutronClient } = - context; - const res = await gaiaClient.signAndBroadcast( - gaiaUserAddress, - [ - { - typeUrl: '/ibc.applications.transfer.v1.MsgTransfer', - value: MsgTransfer.fromPartial({ - sender: gaiaUserAddress, - sourceChannel: 'channel-0', - sourcePort: 'transfer', - receiver: neutronUserAddress, - token: { denom: 'stake', amount: '2000000' }, - timeoutTimestamp: BigInt((Date.now() + 10 * 60 * 1000) * 1e6), - timeoutHeight: { - revisionHeight: BigInt(0), - revisionNumber: BigInt(0), - }, - }), - }, - ], - 1.5, - ); - expect(res.transactionHash).toHaveLength(64); - await waitFor(async () => { - const balances = - await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - neutronUserAddress, - ); - context.neutronIBCDenom = balances.data.balances.find((b) => - b.denom.startsWith('ibc/'), - )?.denom; - return balances.data.balances.length > 1; - }); - expect(context.neutronIBCDenom).toBeTruthy(); - }); - - it('init', async () => { - const { factoryContractClient: contractClient } = context; - const res = await contractClient.init(context.neutronUserAddress, { - base_denom: context.neutronIBCDenom, - core_params: { - idle_min_interval: 60, - puppeteer_timeout: 60, - unbond_batch_switch_time: 6000, - unbonding_safe_period: 10, - unbonding_period: 60, - channel: 'channel-0', - }, - }); - expect(res.transactionHash).toHaveLength(64); - }); - - it('query factory state', async () => { - const { factoryContractClient: contractClient, neutronClient } = context; - const res = await contractClient.queryState(); - expect(res).toBeTruthy(); - const tokenContractInfo = - await neutronClient.CosmwasmWasmV1.query.queryContractInfo( - res.token_contract, - ); - expect(tokenContractInfo.data.contract_info.label).toBe( - 'LIDO-staking-token', - ); - const coreContractInfo = - await neutronClient.CosmwasmWasmV1.query.queryContractInfo( - res.core_contract, - ); - expect(coreContractInfo.data.contract_info.label).toBe('LIDO-staking-core'); - const withdrawalVoucherContractInfo = - await neutronClient.CosmwasmWasmV1.query.queryContractInfo( - res.withdrawal_voucher_contract, - ); - expect(withdrawalVoucherContractInfo.data.contract_info.label).toBe( - 'LIDO-staking-withdrawal-voucher', - ); - const withdrawalManagerContractInfo = - await neutronClient.CosmwasmWasmV1.query.queryContractInfo( - res.withdrawal_manager_contract, - ); - expect(withdrawalManagerContractInfo.data.contract_info.label).toBe( - 'LIDO-staking-withdrawal-manager', - ); - const puppeteerContractInfo = - await neutronClient.CosmwasmWasmV1.query.queryContractInfo( - res.puppeteer_contract, - ); - expect(puppeteerContractInfo.data.contract_info.label).toBe( - 'LIDO-staking-puppeteer', - ); - context.coreContractClient = new LidoCore.Client( - context.client, - res.core_contract, - ); - context.withdrawalVoucherContractClient = new LidoWithdrawalVoucher.Client( - context.client, - res.withdrawal_voucher_contract, - ); - context.withdrawalManagerContractClient = new LidoWithdrawalManager.Client( - context.client, - res.withdrawal_manager_contract, - ); - context.strategyContractClient = new LidoStrategy.Client( - context.client, - res.strategy_contract, - ); - context.tokenContractAddress = res.token_contract; - context.puppeteerContractClient = new LidoPuppeteer.Client( - context.client, - res.puppeteer_contract, - ); - }); - - it('set fees for puppeteer', async () => { - const { neutronUserAddress, factoryContractClient: contractClient } = - context; - const res = await contractClient.updateConfig(neutronUserAddress, { - puppeteer_fees: { - timeout_fee: '10000', - ack_fee: '10000', - recv_fee: '0', - register_fee: '1000000', - }, - }); - expect(res.transactionHash).toHaveLength(64); - }); - - it('register ICA', async () => { - const { puppeteerContractClient, neutronUserAddress } = context; - const res = await puppeteerContractClient.registerICA( - neutronUserAddress, - 1.5, - undefined, - [{ amount: '1000000', denom: 'untrn' }], - ); - expect(res.transactionHash).toHaveLength(64); - let ica = ''; - await waitFor(async () => { - const res = await puppeteerContractClient.queryIca(); - switch (res) { - case 'none': - case 'in_progress': - case 'timeout': - return false; - default: - ica = res.registered.ica_address; - return true; - } - }, 100_000); - expect(ica).toHaveLength(65); - expect(ica.startsWith('cosmos')).toBeTruthy(); - context.icaAddress = ica; - }); - - it('add validators into validators set', async () => { - const { - neutronUserAddress, - factoryContractClient, - validatorAddress, - secondValidatorAddress, - } = context; - const res = await factoryContractClient.proxy( - neutronUserAddress, - { - validator_set: { - update_validators: { - validators: [ - { - valoper_address: validatorAddress, - weight: 1, - }, - { - valoper_address: secondValidatorAddress, - weight: 1, - }, - ], - }, - }, - }, - 1.5, - undefined, - [ - { - amount: '1000000', - denom: 'untrn', - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - }); - - it('query exchange rate', async () => { - const { coreContractClient } = context; - context.exchangeRate = parseFloat( - await coreContractClient.queryExchangeRate(), - ); - expect(context.exchangeRate).toEqual(1); - }); - - it('bond w/o receiver', async () => { - const { - coreContractClient, - neutronClient, - neutronUserAddress, - neutronIBCDenom, - } = context; - const res = await coreContractClient.bond( - neutronUserAddress, - {}, - 1.6, - undefined, - [ - { - amount: '500000', - denom: neutronIBCDenom, - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - await awaitBlocks(`http://127.0.0.1:${context.park.ports.gaia.rpc}`, 1); - const balances = - await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - neutronUserAddress, - ); - expect( - balances.data.balances.find((one) => one.denom.startsWith('factory')), - ).toEqual({ - denom: `factory/${context.tokenContractAddress}/lido`, - amount: String(Math.floor(500_000 / context.exchangeRate)), - }); - }); - - it('bond with receiver', async () => { - const { - coreContractClient, - neutronClient, - neutronUserAddress, - neutronIBCDenom, - neutronSecondUserAddress, - } = context; - const res = await coreContractClient.bond( - neutronUserAddress, - { receiver: neutronSecondUserAddress }, - 1.6, - undefined, - [ - { - amount: '500000', - denom: neutronIBCDenom, - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - await awaitBlocks(`http://127.0.0.1:${context.park.ports.gaia.rpc}`, 1); - const balances = - await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - neutronSecondUserAddress, - ); - const ldBalance = balances.data.balances.find((one) => - one.denom.startsWith('factory'), - ); - expect(ldBalance).toEqual({ - denom: `factory/${context.tokenContractAddress}/lido`, - amount: String(Math.floor(500_000 / context.exchangeRate)), - }); - context.ldDenom = ldBalance?.denom; - }); - - it('unbond', async () => { - const { coreContractClient, neutronUserAddress, ldDenom } = context; - const res = await coreContractClient.unbond( - neutronUserAddress, - 1.6, - undefined, - [ - { - amount: Math.floor(500_000 / context.exchangeRate).toString(), - denom: ldDenom, - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - }); - - it('validate unbonding batch', async () => { - const { coreContractClient, neutronUserAddress } = context; - const batch = await coreContractClient.queryUnbondBatch({ - batch_id: '0', - }); - expect(batch).toBeTruthy(); - expect(batch).toEqual({ - slashing_effect: null, - created: expect.any(Number), - expected_release: 0, - status: 'new', - total_amount: '500000', - expected_amount: '500000', - unbond_items: [ - { - amount: '500000', - expected_amount: '500000', - sender: neutronUserAddress, - }, - ], - unbonded_amount: null, - withdrawed_amount: null, - }); - }); - describe('state machine', () => { - const ica: { balance?: number } = {}; - describe('prepare', () => { - it('get ICA balance', async () => { - const { gaiaClient } = context; - const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); - ica.balance = parseInt(res.amount); - expect(ica.balance).toEqual(0); - }); - it('deploy pump', async () => { - const { client, account, neutronUserAddress } = context; - const resUpload = await client.upload( - account.address, - fs.readFileSync(join(__dirname, '../../../artifacts/lido_pump.wasm')), - 1.5, - ); - expect(resUpload.codeId).toBeGreaterThan(0); - const { codeId } = resUpload; - const res = await LidoPump.Client.instantiate( - client, - neutronUserAddress, - codeId, - { - connection_id: 'connection-0', - ibc_fees: { - timeout_fee: '10000', - ack_fee: '10000', - recv_fee: '0', - register_fee: '1000000', - }, - local_denom: 'stake', - timeout: { - local: 60, - remote: 60, - }, - }, - 'Lido-staking-pump', - [], - 1.5, - ); - expect(res.contractAddress).toHaveLength(66); - context.pumpContractClient = new LidoPump.Client( - client, - res.contractAddress, - ); - const resFactory = await context.factoryContractClient.updateConfig( - neutronUserAddress, - { - core: { - pump_address: context.pumpContractClient.contractAddress, - }, - }, - ); - expect(resFactory.transactionHash).toHaveLength(64); - }); - it('get machine state', async () => { - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('idle'); - }); - }); - describe('first cycle', () => { - it('tick', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [ - { - amount: '1000000', - denom: 'untrn', - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('transfering'); - }); - it('second tick is failed bc no response from puppeteer yet', async () => { - const { neutronUserAddress } = context; - await expect( - context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ), - ).rejects.toThrowError(/Puppeteer response is not received/); - }); - it('state of fsm is transfering', async () => { - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('transfering'); - }); - it('wait for response from puppeteer', async () => { - let response; - await waitFor(async () => { - try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); - } catch (e) { - // - } - return !!response; - }, 100_000); - }); - it('get ICA increased balance', async () => { - const { gaiaClient } = context; - const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); - const balance = parseInt(res.amount); - expect(balance - 1000000).toEqual(ica.balance); - ica.balance = balance; - }); - it('wait for balances to come', async () => { - let res; - await waitFor(async () => { - try { - res = await context.puppeteerContractClient.queryExtention({ - msg: { - balances: {}, - }, - }); - } catch (e) { - // - } - return res && res[0].coins.length !== 0; - }, 100_000); - }); - it('second tick goes to staking', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [ - { - amount: '1000000', - denom: 'untrn', - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('staking'); - }); - it('second tick is failed bc no response from puppeteer yet', async () => { - const { neutronUserAddress } = context; - await expect( - context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ), - ).rejects.toThrowError(/Puppeteer response is not received/); - }); - it('wait for response from puppeteer', async () => { - let response; - await waitFor(async () => { - try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); - } catch (e) { - // - } - return !!response; - }, 100_000); - }); - it('query strategy contract to see delegations', async () => { - await waitFor(async () => { - try { - await context.strategyContractClient.queryCalcWithdraw({ - withdraw: '500000', - }); - return true; - } catch (e) { - return false; - } - }, 100_000); - }); - it('third tick goes to unbonding', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [ - { - amount: '1000000', - denom: 'untrn', - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('unbonding'); - }); - it('third tick is failed bc no response from puppeteer yet', async () => { - const { neutronUserAddress } = context; - await expect( - context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ), - ).rejects.toThrowError(/Puppeteer response is not received/); - }); - it('query unbonding batch', async () => { - const batch = await context.coreContractClient.queryUnbondBatch({ - batch_id: '0', - }); - expect(batch).toBeTruthy(); - expect(batch).toEqual({ - slashing_effect: null, - status: 'unbond_requested', - created: expect.any(Number), - expected_release: 0, - total_amount: '500000', - expected_amount: '500000', - unbond_items: [ - { - amount: '500000', - expected_amount: '500000', - sender: context.neutronUserAddress, - }, - ], - unbonded_amount: null, - withdrawed_amount: null, - }); - }); - it('wait for response from puppeteer', async () => { - let response; - await waitFor(async () => { - try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); - } catch (e) { - // - } - return !!response; - }, 100_000); - }); - it('next tick goes to idle', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('idle'); - }); - it('verify that unbonding batch is in unbonding state', async () => { - const batch = await context.coreContractClient.queryUnbondBatch({ - batch_id: '0', - }); - expect(batch).toBeTruthy(); - expect(batch).toEqual({ - slashing_effect: null, - status: 'unbonding', - created: expect.any(Number), - expected_release: expect.any(Number), - total_amount: '500000', - expected_amount: '500000', - unbond_items: [ - { - amount: '500000', - expected_amount: '500000', - sender: context.neutronUserAddress, - }, - ], - unbonded_amount: null, - withdrawed_amount: null, - }); - }); - }); - describe('second cycle', () => { - let balance = 0; - it('get ICA balance', async () => { - const { gaiaClient } = context; - const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); - balance = parseInt(res.amount); - }); - it('wait for 30 seconds', async () => { - await sleep(30_000); - }); - it('idle tick', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('claiming'); - }); - it('wait for response from puppeteer', async () => { - let response; - await waitFor(async () => { - try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); - } catch (e) { - // - } - return !!response; - }, 100_000); - }); - it('get ICA balance', async () => { - const { gaiaClient } = context; - const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); - const newBalance = parseInt(res.amount); - expect(newBalance).toBeGreaterThan(balance); - }); - it('wait for balance to update', async () => { - const [, currentHeight] = - (await context.puppeteerContractClient.queryExtention({ - msg: { - balances: {}, - }, - })) as any; - await waitFor(async () => { - const [, nowHeight] = - (await context.puppeteerContractClient.queryExtention({ - msg: { - balances: {}, - }, - })) as any; - return nowHeight !== currentHeight; - }, 30_000); - }); - it('next tick goes to staking', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('staking'); - }); - it('wait for response from puppeteer', async () => { - let response; - await waitFor(async () => { - try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); - } catch (e) { - // - } - return !!response; - }, 100_000); - }); - it('next tick goes to idle', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('idle'); - }); - }); - describe('third cycle', () => { - let remoteNonNativeDenoms: string[] = []; - it('generate two new tokenfactory tokens and send them to the remote zone', async () => { - const { neutronUserAddress } = context; - await context.park.executeInNetwork( - 'neutron', - `neutrond tx tokenfactory create-denom test1 --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, - ); - await sleep(5_000); - await context.park.executeInNetwork( - 'neutron', - `neutrond tx tokenfactory create-denom test2 --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, - ); - await sleep(5_000); - const denoms = - await context.neutronClient.OsmosisTokenfactoryV1Beta1.query.queryDenomsFromCreator( - neutronUserAddress, - ); - expect(denoms.data.denoms.length).toEqual(2); - await context.park.executeInNetwork( - 'neutron', - `neutrond tx tokenfactory mint 1000000${denoms.data.denoms[0]} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, - ); - await sleep(5_000); - await context.park.executeInNetwork( - 'neutron', - `neutrond tx tokenfactory mint 1000000${denoms.data.denoms[1]} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, - ); - await sleep(5_000); - const balances = - await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - neutronUserAddress, - ); - const tokenFactoryDenoms = balances.data.balances.filter((b) => - b.denom.startsWith('factory/'), - ); - await context.park.executeInNetwork( - 'neutron', - `neutrond tx ibc-transfer transfer transfer channel-0 ${context.icaAddress} 66666${tokenFactoryDenoms[0].denom} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, - ); - await sleep(5_000); - await context.park.executeInNetwork( - 'neutron', - `neutrond tx ibc-transfer transfer transfer channel-0 ${context.icaAddress} 2222${tokenFactoryDenoms[1].denom} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, - ); - await sleep(5_000); - }); - it('wait for balances to come', async () => { - let res: readonly Coin[] = []; - await waitFor(async () => { - res = await context.gaiaClient.getAllBalances(context.icaAddress); - return ( - res.some((b) => b.amount === '66666') && - res.some((b) => b.amount === '2222') - ); - }, 30_000); - remoteNonNativeDenoms = [ - res.find((b) => b.amount === '66666').denom, - res.find((b) => b.amount === '2222').denom, - ]; - }); - it('setup non-native receivers', async () => { - const { factoryContractClient, neutronUserAddress } = context; - const res = await factoryContractClient.proxy( - neutronUserAddress, - { - core: { - update_non_native_rewards_receivers: { - items: remoteNonNativeDenoms.map((denom) => ({ - denom, - address: context.gaiaUserAddress, - min_amount: '10000', - fee: '0.1', - fee_address: context.gaiaUserAddress2, - })), - }, - }, - }, - 1.5, - undefined, - [ - { - amount: '1000000', - denom: 'untrn', - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - }); - it('update idle interval', async () => { - const { factoryContractClient, neutronUserAddress } = context; - const res = await factoryContractClient.updateConfig( - neutronUserAddress, - { - core: { - idle_min_interval: 10000, - }, - }, - ); - expect(res.transactionHash).toHaveLength(64); - }); - it('wait for non-native balances to come', async () => { - await waitFor(async () => { - try { - const res: any = - await context.puppeteerContractClient.queryExtention({ - msg: { - non_native_rewards_balances: {}, - }, - }); - return res.length == 2; - } catch (e) { - // - } - }); - }); - it('tick', async () => { - const { neutronUserAddress } = context; - const res = await context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); - const state = await context.coreContractClient.queryContractState(); - expect(state).toEqual('idle'); - }); - it('wait for the response from puppeteer', async () => { - let response: ResponseHookMsg; - await waitFor(async () => { - try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); - } catch (e) { - // - } - return !!response; - }, 30_000); - expect(response).toBeTruthy(); - expect(response).toHaveProperty('success'); - }); - it('check balances', async () => { - const { gaiaClient } = context; - const receiverBalance = await gaiaClient.getBalance( - context.gaiaUserAddress, - remoteNonNativeDenoms[0], - ); - expect(receiverBalance.amount).toEqual('60000'); - const feeBalance = await gaiaClient.getBalance( - context.gaiaUserAddress2, - remoteNonNativeDenoms[0], - ); - expect(feeBalance.amount).toEqual('6666'); - // this one is still on ICA as amount is below min_amount - const icaBalance = await gaiaClient.getBalance( - context.icaAddress, - remoteNonNativeDenoms[1], - ); - expect(icaBalance.amount).toEqual('2222'); - }); - it('wait for balances to update', async () => { - await waitFor(async () => { - const res: any = await context.puppeteerContractClient.queryExtention( - { - msg: { - non_native_rewards_balances: {}, - }, - }, - ); - return res[0].coins.length === 1; - }); - }, 30_000); - it('tick should fail', async () => { - const { neutronUserAddress } = context; - await expect( - context.coreContractClient.tick( - neutronUserAddress, - 1.5, - undefined, - [], - ), - ).rejects.toThrowError(/Idle min interval is not reached/); - }); - }); - }); -}); diff --git a/integration_tests/src/testcases/core.test.ts b/integration_tests/src/testcases/core.test.ts index 36bf3169..6ea4a443 100644 --- a/integration_tests/src/testcases/core.test.ts +++ b/integration_tests/src/testcases/core.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, beforeAll, afterAll } from 'vitest'; import { - LidoAutoWithdrawer, LidoCore, LidoFactory, LidoPump, @@ -15,22 +14,31 @@ import { BankExtension, setupStakingExtension, setupBankExtension, - IndexedTx, SigningStargateClient, + IndexedTx, } from '@cosmjs/stargate'; import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx'; import { join } from 'path'; import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; import { Client as NeutronClient } from '@neutron-org/client-ts'; -import { AccountData, DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; +import { + AccountData, + Coin, + DirectSecp256k1HdWallet, +} from '@cosmjs/proto-signing'; import { GasPrice } from '@cosmjs/stargate'; import { awaitBlocks, setupPark } from '../testSuite'; import fs from 'fs'; import Cosmopark from '@neutron-org/cosmopark'; import { waitFor } from '../helpers/waitFor'; -import { UnbondBatch } from '../generated/contractLib/lidoCore'; +import { + ResponseHookMsg, + ResponseHookSuccessMsg, + UnbondBatch, +} from '../generated/contractLib/lidoCore'; import { stringToPath } from '@cosmjs/crypto'; +import { sleep } from '../helpers/sleep'; const LidoFactoryClass = LidoFactory.Client; const LidoCoreClass = LidoCore.Client; @@ -39,7 +47,6 @@ const LidoPuppeteerClass = LidoPuppeteer.Client; const LidoStrategyClass = LidoStrategy.Client; const LidoWithdrawalVoucherClass = LidoWithdrawalVoucher.Client; const LidoWithdrawalManagerClass = LidoWithdrawalManager.Client; -const LidoAutoWithdrawerClass = LidoAutoWithdrawer.Client; describe('Core', () => { const context: { @@ -47,6 +54,7 @@ describe('Core', () => { contractAddress?: string; wallet?: DirectSecp256k1HdWallet; gaiaWallet?: DirectSecp256k1HdWallet; + gaiaWallet2?: DirectSecp256k1HdWallet; factoryContractClient?: InstanceType; coreContractClient?: InstanceType; strategyContractClient?: InstanceType; @@ -58,12 +66,12 @@ describe('Core', () => { withdrawalManagerContractClient?: InstanceType< typeof LidoWithdrawalManagerClass >; - autoWithdrawerContractClient?: InstanceType; account?: AccountData; icaAddress?: string; client?: SigningCosmWasmClient; gaiaClient?: SigningStargateClient; gaiaUserAddress?: string; + gaiaUserAddress2?: string; gaiaQueryClient?: QueryClient & StakingExtension & BankExtension; neutronClient?: InstanceType; neutronUserAddress?: string; @@ -89,7 +97,13 @@ describe('Core', () => { } = { codeIds: {} }; beforeAll(async () => { - context.park = await setupPark('core', ['neutron', 'gaia'], true); + context.park = await setupPark( + 'core', + ['neutron', 'gaia'], + true, + true, + true, + ); context.wallet = await DirectSecp256k1HdWallet.fromMnemonic( context.park.config.wallets.demowallet1.mnemonic, { @@ -102,6 +116,12 @@ describe('Core', () => { prefix: 'cosmos', }, ); + context.gaiaWallet2 = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.wallets.demo1.mnemonic, + { + prefix: 'cosmos', + }, + ); context.account = (await context.wallet.getAccounts())[0]; context.neutronClient = new NeutronClient({ apiURL: `http://127.0.0.1:${context.park.ports.neutron.rest}`, @@ -130,7 +150,6 @@ describe('Core', () => { setupStakingExtension, setupBankExtension, ); - const secondWallet = await DirectSecp256k1HdWallet.fromMnemonic( context.park.config.wallets.demo2.mnemonic, { @@ -148,6 +167,7 @@ describe('Core', () => { it('instantiate', async () => { const { client, account } = context; + context.codeIds = {}; { const res = await client.upload( @@ -244,6 +264,7 @@ describe('Core', () => { expect(res.codeId).toBeGreaterThan(0); context.codeIds.rewardsManager = res.codeId; } + const res = await client.upload( account.address, fs.readFileSync(join(__dirname, '../../../artifacts/lido_factory.wasm')), @@ -298,6 +319,9 @@ describe('Core', () => { context.gaiaUserAddress = ( await context.gaiaWallet.getAccounts() )[0].address; + context.gaiaUserAddress2 = ( + await context.gaiaWallet2.getAccounts() + )[0].address; context.neutronUserAddress = ( await context.wallet.getAccounts() )[0].address; @@ -366,11 +390,11 @@ describe('Core', () => { const res = await contractClient.init(context.neutronUserAddress, { base_denom: context.neutronIBCDenom, core_params: { - idle_min_interval: 1, + idle_min_interval: 60, puppeteer_timeout: 60, - unbond_batch_switch_time: 6000, + unbond_batch_switch_time: 240, unbonding_safe_period: 10, - unbonding_period: 60, + unbonding_period: 360, channel: 'channel-0', }, }); @@ -436,9 +460,11 @@ describe('Core', () => { res.puppeteer_contract, ); }); + it('set fees for puppeteer', async () => { - const { neutronUserAddress, factoryContractClient } = context; - const res = await factoryContractClient.updateConfig(neutronUserAddress, { + const { neutronUserAddress, factoryContractClient: contractClient } = + context; + const res = await contractClient.updateConfig(neutronUserAddress, { puppeteer_fees: { timeout_fee: '20000', ack_fee: '10000', @@ -506,27 +532,6 @@ describe('Core', () => { expect(ica.startsWith('cosmos')).toBeTruthy(); context.icaAddress = ica; }); - it('register balance ICQ', async () => { - const { factoryContractClient, neutronUserAddress } = context; - const res = await factoryContractClient.proxy( - neutronUserAddress, - { - validator_set: { - update_validators: { validators: [] }, - }, - }, - 1.5, - undefined, - [ - { - amount: '1000000', - denom: 'untrn', - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - }); - it('query exchange rate', async () => { const { coreContractClient } = context; context.exchangeRate = parseFloat( @@ -535,75 +540,6 @@ describe('Core', () => { expect(context.exchangeRate).toEqual(1); }); - it('bond w/o receiver', async () => { - const { - coreContractClient, - neutronClient, - neutronUserAddress, - neutronIBCDenom, - } = context; - const res = await coreContractClient.bond( - neutronUserAddress, - {}, - 1.6, - undefined, - [ - { - amount: '500000', - denom: neutronIBCDenom, - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - await awaitBlocks(`http://127.0.0.1:${context.park.ports.gaia.rpc}`, 1); - const balances = - await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - neutronUserAddress, - ); - expect( - balances.data.balances.find((one) => one.denom.startsWith('factory')), - ).toEqual({ - denom: `factory/${context.tokenContractAddress}/lido`, - amount: String(Math.floor(500_000 / context.exchangeRate)), - }); - }); - - it('bond with receiver', async () => { - const { - coreContractClient, - neutronClient, - neutronUserAddress, - neutronIBCDenom, - neutronSecondUserAddress, - } = context; - const res = await coreContractClient.bond( - neutronUserAddress, - { receiver: neutronSecondUserAddress }, - 1.6, - undefined, - [ - { - amount: '500000', - denom: neutronIBCDenom, - }, - ], - ); - expect(res.transactionHash).toHaveLength(64); - await awaitBlocks(`http://127.0.0.1:${context.park.ports.gaia.rpc}`, 1); - const balances = - await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - neutronSecondUserAddress, - ); - const ldBalance = balances.data.balances.find((one) => - one.denom.startsWith('factory'), - ); - expect(ldBalance).toEqual({ - denom: `factory/${context.tokenContractAddress}/lido`, - amount: String(Math.floor(500_000 / context.exchangeRate)), - }); - context.ldDenom = ldBalance?.denom; - }); - it('delegate tokens on gaia side', async () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic( context.park.config.master_mnemonic, @@ -680,7 +616,7 @@ describe('Core', () => { await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( neutronUserAddress, ); - return balances.data.balances.length > 3; + return balances.data.balances.length > 2; }); const shareOnNeutron = balances.data.balances.find( (b) => b.amount === '600000', @@ -705,23 +641,50 @@ describe('Core', () => { ); await expect(res).rejects.toThrowError(/Invalid denom/); }); - it('register validator', async () => { - const { factoryContractClient, neutronUserAddress, validatorAddress } = - context; - const res = await factoryContractClient.proxy(neutronUserAddress, { - validator_set: { - update_validator: { - validator: { - valoper_address: validatorAddress, - weight: 1, + it('add validators into validators set', async () => { + const { + neutronUserAddress, + factoryContractClient, + validatorAddress, + secondValidatorAddress, + } = context; + const res = await factoryContractClient.proxy( + neutronUserAddress, + { + validator_set: { + update_validators: { + validators: [ + { + valoper_address: validatorAddress, + weight: 1, + }, + { + valoper_address: secondValidatorAddress, + weight: 1, + }, + ], }, }, }, - }); - expect(res).toBeTruthy(); + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); }); - it('bond tokenized share from registered validator', async () => { - const { coreContractClient, neutronUserAddress } = context; + + it('bond w/o receiver', async () => { + const { + coreContractClient, + neutronClient, + neutronUserAddress, + neutronIBCDenom, + } = context; const res = await coreContractClient.bond( neutronUserAddress, {}, @@ -729,33 +692,78 @@ describe('Core', () => { undefined, [ { - amount: '20000', - denom: context.tokenizedDenomOnNeutron, + amount: '500000', + denom: neutronIBCDenom, + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + await awaitBlocks(`http://127.0.0.1:${context.park.ports.gaia.rpc}`, 1); + const balances = + await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronUserAddress, + ); + expect( + balances.data.balances.find((one) => one.denom.startsWith('factory')), + ).toEqual({ + denom: `factory/${context.tokenContractAddress}/lido`, + amount: String(Math.floor(500_000 / context.exchangeRate)), + }); + }); + + it('bond with receiver', async () => { + const { + coreContractClient, + neutronClient, + neutronUserAddress, + neutronIBCDenom, + neutronSecondUserAddress, + } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + { receiver: neutronSecondUserAddress }, + 1.6, + undefined, + [ + { + amount: '500000', + denom: neutronIBCDenom, }, ], ); expect(res.transactionHash).toHaveLength(64); + await awaitBlocks(`http://127.0.0.1:${context.park.ports.gaia.rpc}`, 1); + const balances = + await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronSecondUserAddress, + ); + const ldBalance = balances.data.balances.find((one) => + one.denom.startsWith('factory'), + ); + expect(ldBalance).toEqual({ + denom: `factory/${context.tokenContractAddress}/lido`, + amount: String(Math.floor(500_000 / context.exchangeRate)), + }); + context.ldDenom = ldBalance?.denom; }); it('unbond', async () => { const { coreContractClient, neutronUserAddress, ldDenom } = context; - let res = await coreContractClient.unbond( neutronUserAddress, 1.6, undefined, [ { - amount: Math.floor(300_000 / context.exchangeRate).toString(), + amount: Math.floor(200_000 / context.exchangeRate).toString(), denom: ldDenom, }, ], ); expect(res.transactionHash).toHaveLength(64); - const amount = Math.floor(100_000 / context.exchangeRate).toString(); res = await coreContractClient.unbond(neutronUserAddress, 1.6, undefined, [ { - amount, + amount: Math.floor(300_000 / context.exchangeRate).toString(), denom: ldDenom, }, ]); @@ -773,17 +781,17 @@ describe('Core', () => { created: expect.any(Number), expected_release: 0, status: 'new', - total_amount: '400000', - expected_amount: '400000', + total_amount: '500000', + expected_amount: '500000', unbond_items: [ { - amount: '300000', - expected_amount: '300000', + amount: '200000', + expected_amount: '200000', sender: neutronUserAddress, }, { - amount: '100000', - expected_amount: '100000', + amount: '300000', + expected_amount: '300000', sender: neutronUserAddress, }, ], @@ -791,246 +799,949 @@ describe('Core', () => { withdrawed_amount: null, }); }); - - it('validate NFT', async () => { - const { withdrawalVoucherContractClient, neutronUserAddress } = context; - const vouchers = await withdrawalVoucherContractClient.queryTokens({ - owner: context.neutronUserAddress, - }); - expect(vouchers.tokens.length).toBe(2); - expect(vouchers.tokens[0]).toBe(`0_${neutronUserAddress}_1`); - let tokenId = vouchers.tokens[0]; - let voucher = await withdrawalVoucherContractClient.queryNftInfo({ - token_id: tokenId, - }); - expect(voucher).toBeTruthy(); - expect(voucher).toMatchObject({ - extension: { - amount: '300000', - attributes: [ + describe('state machine', () => { + const ica: { balance?: number } = {}; + describe('prepare', () => { + it('get ICA balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + ica.balance = parseInt(res.amount); + expect(ica.balance).toEqual(0); + }); + it('deploy pump', async () => { + const { client, account, neutronUserAddress } = context; + const resUpload = await client.upload( + account.address, + fs.readFileSync(join(__dirname, '../../../artifacts/lido_pump.wasm')), + 1.5, + ); + expect(resUpload.codeId).toBeGreaterThan(0); + const { codeId } = resUpload; + const res = await LidoPump.Client.instantiate( + client, + neutronUserAddress, + codeId, { - display_type: null, - trait_type: 'unbond_batch_id', - value: '0', + connection_id: 'connection-0', + ibc_fees: { + timeout_fee: '10000', + ack_fee: '10000', + recv_fee: '0', + register_fee: '1000000', + }, + local_denom: 'untrn', + timeout: { + local: 60, + remote: 60, + }, + dest_address: + context.withdrawalManagerContractClient.contractAddress, + dest_port: 'transfer', + dest_channel: 'channel-0', + refundee: neutronUserAddress, + owner: account.address, }, + 'Lido-staking-pump', + [], + 1.5, + ); + expect(res.contractAddress).toHaveLength(66); + context.pumpContractClient = new LidoPump.Client( + client, + res.contractAddress, + ); + await context.pumpContractClient.registerICA( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + let ica = ''; + await waitFor(async () => { + const res = await context.pumpContractClient.queryIca(); + switch (res) { + case 'none': + case 'in_progress': + case 'timeout': + return false; + default: + ica = res.registered.ica_address; + return true; + } + }, 50_000); + expect(ica).toHaveLength(65); + expect(ica.startsWith('cosmos')).toBeTruthy(); + const resFactory = await context.factoryContractClient.updateConfig( + neutronUserAddress, { - display_type: null, - trait_type: 'received_amount', - value: '300000', + core: { + pump_address: ica, + }, }, + ); + expect(resFactory.transactionHash).toHaveLength(64); + }); + it('get machine state', async () => { + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + }); + describe('first cycle', () => { + it('tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('transfering'); + }); + it('second tick is failed bc no response from puppeteer yet', async () => { + const { neutronUserAddress } = context; + await expect( + context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ), + ).rejects.toThrowError(/Puppeteer response is not received/); + }); + it('state of fsm is transfering', async () => { + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('transfering'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('get ICA increased balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + const balance = parseInt(res.amount); + expect(balance - 1000000).toEqual(ica.balance); + ica.balance = balance; + }); + it('wait for balances to come', async () => { + let res; + await waitFor(async () => { + try { + res = await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + }); + } catch (e) { + // + } + return res && res[0].coins.length !== 0; + }, 100_000); + }); + it('second tick goes to staking', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + }); + it('second tick is failed bc no response from puppeteer yet', async () => { + const { neutronUserAddress } = context; + await expect( + context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ), + ).rejects.toThrowError(/Puppeteer response is not received/); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('query strategy contract to see delegations', async () => { + await waitFor(async () => { + try { + await context.strategyContractClient.queryCalcWithdraw({ + withdraw: '500000', + }); + return true; + } catch (e) { + return false; + } + }, 100_000); + }); + it('third tick goes to unbonding', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('unbonding'); + }); + it('third tick is failed bc no response from puppeteer yet', async () => { + const { neutronUserAddress } = context; + await expect( + context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ), + ).rejects.toThrowError(/Puppeteer response is not received/); + }); + it('query unbonding batch', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'unbond_requested', + created: expect.any(Number), + expected_release: 0, + total_amount: '500000', + expected_amount: '500000', + unbond_items: [ + { + amount: '200000', + expected_amount: '200000', + sender: context.neutronUserAddress, + }, + { + amount: '300000', + expected_amount: '300000', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('next tick goes to idle', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + it('verify that unbonding batch is in unbonding state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'unbonding', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '500000', + expected_amount: '500000', + unbond_items: [ + { + amount: '200000', + expected_amount: '200000', + sender: context.neutronUserAddress, + }, + { + amount: '300000', + expected_amount: '300000', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + }); + describe('second cycle', () => { + let balance = 0; + it('get ICA balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + balance = parseInt(res.amount); + }); + it('wait for 30 seconds', async () => { + await sleep(30_000); + }); + it('idle tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('claiming'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('get ICA balance', async () => { + const { gaiaClient } = context; + const res = await gaiaClient.getBalance(context.icaAddress, 'stake'); + const newBalance = parseInt(res.amount); + expect(newBalance).toBeGreaterThan(balance); + }); + it('wait for balance to update', async () => { + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 30_000); + }); + it('next tick goes to staking', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + }); + it('wait for response from puppeteer', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('next tick goes to idle', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + }); + describe('third cycle', () => { + let remoteNonNativeDenoms: string[] = []; + it('generate two new tokenfactory tokens and send them to the remote zone', async () => { + const { neutronUserAddress } = context; + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory create-denom test1 --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory create-denom test2 --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + const denoms = + await context.neutronClient.OsmosisTokenfactoryV1Beta1.query.queryDenomsFromCreator( + neutronUserAddress, + ); + expect(denoms.data.denoms.length).toEqual(2); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory mint 1000000${denoms.data.denoms[0]} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx tokenfactory mint 1000000${denoms.data.denoms[1]} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronUserAddress, + ); + const tokenFactoryDenoms = balances.data.balances.filter((b) => + b.denom.startsWith('factory/'), + ); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx ibc-transfer transfer transfer channel-0 ${context.icaAddress} 66666${tokenFactoryDenoms[0].denom} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + await context.park.executeInNetwork( + 'neutron', + `neutrond tx ibc-transfer transfer transfer channel-0 ${context.icaAddress} 2222${tokenFactoryDenoms[1].denom} --from ${neutronUserAddress} --yes --chain-id ntrntest --gas auto --gas-adjustment 1.6 --fees 10000untrn --home=/opt --keyring-backend=test --output json`, + ); + await sleep(5_000); + }); + it('wait for balances to come', async () => { + let res: readonly Coin[] = []; + await waitFor(async () => { + res = await context.gaiaClient.getAllBalances(context.icaAddress); + return ( + res.some((b) => b.amount === '66666') && + res.some((b) => b.amount === '2222') + ); + }, 30_000); + remoteNonNativeDenoms = [ + res.find((b) => b.amount === '66666').denom, + res.find((b) => b.amount === '2222').denom, + ]; + }); + it('setup non-native receivers', async () => { + const { factoryContractClient, neutronUserAddress } = context; + const res = await factoryContractClient.proxy( + neutronUserAddress, { - display_type: null, - trait_type: 'expected_amount', - value: '300000', + core: { + update_non_native_rewards_receivers: { + items: remoteNonNativeDenoms.map((denom) => ({ + denom, + address: context.gaiaUserAddress, + min_amount: '10000', + fee: '0.1', + fee_address: context.gaiaUserAddress2, + })), + }, + }, }, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + }); + it('update idle interval', async () => { + const { factoryContractClient, neutronUserAddress } = context; + const res = await factoryContractClient.updateConfig( + neutronUserAddress, { - display_type: null, - trait_type: 'exchange_rate', - value: '1', + core: { + idle_min_interval: 10000, + }, }, - ], - batch_id: '0', - description: 'Withdrawal voucher', - expected_amount: '300000', - name: 'LDV voucher', - }, - token_uri: null, - }); - expect(vouchers.tokens[1]).toBe(`0_${neutronUserAddress}_2`); - tokenId = vouchers.tokens[1]; - voucher = await withdrawalVoucherContractClient.queryNftInfo({ - token_id: tokenId, + ); + expect(res.transactionHash).toHaveLength(64); + }); + it('wait for non-native balances to come', async () => { + await waitFor(async () => { + try { + const res: any = + await context.puppeteerContractClient.queryExtention({ + msg: { + non_native_rewards_balances: {}, + }, + }); + return res[0].coins.length == 2; + } catch (e) { + // + } + }); + }); + it('tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + it('wait for the response from puppeteer', async () => { + let response: ResponseHookMsg; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 30_000); + expect(response).toBeTruthy(); + expect(response).toHaveProperty('success'); + }); + it('check balances', async () => { + const { gaiaClient } = context; + const receiverBalance = await gaiaClient.getBalance( + context.gaiaUserAddress, + remoteNonNativeDenoms[0], + ); + expect(receiverBalance.amount).toEqual('60000'); + const feeBalance = await gaiaClient.getBalance( + context.gaiaUserAddress2, + remoteNonNativeDenoms[0], + ); + expect(feeBalance.amount).toEqual('6666'); + // this one is still on ICA as amount is below min_amount + const icaBalance = await gaiaClient.getBalance( + context.icaAddress, + remoteNonNativeDenoms[1], + ); + expect(icaBalance.amount).toEqual('2222'); + }); + it('wait for balances to update', async () => { + await waitFor(async () => { + const res: any = await context.puppeteerContractClient.queryExtention( + { + msg: { + non_native_rewards_balances: {}, + }, + }, + ); + return res[0].coins.length === 1; + }); + }, 30_000); + it('tick should fail', async () => { + const { neutronUserAddress } = context; + await expect( + context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ), + ).rejects.toThrowError(/Idle min interval is not reached/); + }); }); - expect(voucher).toBeTruthy(); - expect(voucher).toMatchObject({ - extension: { - amount: '100000', - attributes: [ - { - display_type: null, - trait_type: 'unbond_batch_id', - value: '0', + + describe('fourth cycle', () => { + let previousResponse: ResponseHookSuccessMsg; + + it('validate NFT', async () => { + const { withdrawalVoucherContractClient, neutronUserAddress } = context; + const vouchers = await withdrawalVoucherContractClient.queryTokens({ + owner: context.neutronUserAddress, + }); + expect(vouchers.tokens.length).toBe(2); + expect(vouchers.tokens[0]).toBe(`0_${neutronUserAddress}_1`); + let tokenId = vouchers.tokens[0]; + let voucher = await withdrawalVoucherContractClient.queryNftInfo({ + token_id: tokenId, + }); + expect(voucher).toBeTruthy(); + expect(voucher).toMatchObject({ + extension: { + amount: '200000', + attributes: [ + { + display_type: null, + trait_type: 'unbond_batch_id', + value: '0', + }, + { + display_type: null, + trait_type: 'received_amount', + value: '200000', + }, + { + display_type: null, + trait_type: 'expected_amount', + value: '200000', + }, + { + display_type: null, + trait_type: 'exchange_rate', + value: '1', + }, + ], + batch_id: '0', + description: 'Withdrawal voucher', + expected_amount: '200000', + name: 'LDV voucher', }, - { - display_type: null, - trait_type: 'received_amount', - value: '100000', + token_uri: null, + }); + expect(vouchers.tokens[1]).toBe(`0_${neutronUserAddress}_2`); + tokenId = vouchers.tokens[1]; + voucher = await withdrawalVoucherContractClient.queryNftInfo({ + token_id: tokenId, + }); + expect(voucher).toBeTruthy(); + expect(voucher).toMatchObject({ + extension: { + amount: '300000', + attributes: [ + { + display_type: null, + trait_type: 'unbond_batch_id', + value: '0', + }, + { + display_type: null, + trait_type: 'received_amount', + value: '300000', + }, + { + display_type: null, + trait_type: 'expected_amount', + value: '300000', + }, + { + display_type: null, + trait_type: 'exchange_rate', + value: '1', + }, + ], + batch_id: '0', + description: 'Withdrawal voucher', + expected_amount: '300000', + name: 'LDV voucher', }, + token_uri: null, + }); + }); + it('bond tokenized share from registered validator', async () => { + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '20000', + denom: context.tokenizedDenomOnNeutron, + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + }); + it('try to withdraw before withdrawn', async () => { + const { withdrawalVoucherContractClient, neutronUserAddress } = context; + const tokenId = `0_${neutronUserAddress}_1`; + await expect( + withdrawalVoucherContractClient.sendNft(neutronUserAddress, { + token_id: tokenId, + contract: context.withdrawalManagerContractClient.contractAddress, + msg: Buffer.from( + JSON.stringify({ + withdraw: {}, + }), + ).toString('base64'), + }), + ).rejects.toThrowError(/is not withdrawn yet/); + }); + it('update idle interval', async () => { + const { factoryContractClient, neutronUserAddress } = context; + const res = await factoryContractClient.updateConfig( + neutronUserAddress, { - display_type: null, - trait_type: 'expected_amount', - value: '100000', + core: { + idle_min_interval: 10, + }, }, + ); + expect(res.transactionHash).toHaveLength(64); + await sleep(10 * 1000); + }); + it('wait until unbonding period is finished', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + const currentTime = Math.floor(Date.now() / 1000); + if (batchInfo.expected_release > currentTime) { + const diffMs = (batchInfo.expected_release - currentTime + 1) * 1000; + await sleep(diffMs); + } + }); + it('wait until fresh ICA balance is delivered', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + await waitFor(async () => { + const icaTs = Math.floor( + ( + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any + )[2] / 1e9, + ); + return icaTs > batchInfo.expected_release; + }, 50_000); + }); + it('tick', async () => { + const { coreContractClient, neutronUserAddress } = context; + previousResponse = ( + (await coreContractClient.queryLastPuppeteerResponse()) as { + success: ResponseHookSuccessMsg; + } + ).success; + await coreContractClient.tick(neutronUserAddress, 1.5, undefined, []); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('claiming'); + }); + it('wait for the response from puppeteer', async () => { + let response: ResponseHookMsg; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return ( + (response as { success: ResponseHookSuccessMsg }).success + .request_id > previousResponse.request_id + ); + }, 30_000); + }); + it('wait for balance to update', async () => { + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 30_000); + }); + it('tick', async () => { + const { coreContractClient, neutronUserAddress } = context; + await coreContractClient.tick(neutronUserAddress, 1.5, undefined, []); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + }); + it('validate unbonding batch', async () => { + const { coreContractClient, neutronUserAddress } = context; + const batch = await coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(batch).toEqual({ + slashing_effect: '1', + status: 'withdrawn', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '500000', + expected_amount: '500000', + unbond_items: [ + { + amount: '200000', + expected_amount: '200000', + sender: neutronUserAddress, + }, + { + amount: '300000', + expected_amount: '300000', + sender: neutronUserAddress, + }, + ], + unbonded_amount: '500000', + withdrawed_amount: null, + }); + }); + it('withdraw with non funded withdrawal manager', async () => { + const { + withdrawalVoucherContractClient: voucherContractClient, + neutronUserAddress, + } = context; + const tokenId = `0_${neutronUserAddress}_1`; + await expect( + voucherContractClient.sendNft(neutronUserAddress, { + token_id: tokenId, + contract: context.withdrawalManagerContractClient.contractAddress, + msg: Buffer.from( + JSON.stringify({ + withdraw: {}, + }), + ).toString('base64'), + }), + ).rejects.toThrowError(/spendable balance {2}is smaller/); + }); + it('fund withdrawal manager', async () => { + const { pumpContractClient, neutronUserAddress } = context; + await pumpContractClient.push( + neutronUserAddress, { - display_type: null, - trait_type: 'exchange_rate', - value: '1', + coins: [{ amount: '500000', denom: 'stake' }], }, - ], - batch_id: '0', - description: 'Withdrawal voucher', - expected_amount: '100000', - name: 'LDV voucher', - }, - token_uri: null, - }); - }); - - it('try to withdraw before unbonded', async () => { - const { withdrawalVoucherContractClient, neutronUserAddress } = context; - const tokenId = `0_${neutronUserAddress}_1`; - await expect( - withdrawalVoucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }), - ).rejects.toThrowError(/is not unbonded yet/); - }); - - it('update batch status', async () => { - const { coreContractClient, neutronUserAddress } = context; - await coreContractClient.fakeProcessBatch(neutronUserAddress, { - batch_id: '0', - unbonded_amount: '200000', - }); - }); - - it('validate unbonding batch', async () => { - const { coreContractClient, neutronUserAddress } = context; - const batch = await coreContractClient.queryUnbondBatch({ - batch_id: '0', - }); - expect(batch).toBeTruthy(); - expect(batch).toEqual({ - slashing_effect: '0.5', - status: 'unbonded', - created: expect.any(Number), - expected_release: 0, - total_amount: '400000', - expected_amount: '400000', - unbond_items: [ - { - amount: '300000', - expected_amount: '300000', - sender: neutronUserAddress, - }, - { - amount: '100000', - expected_amount: '100000', - sender: neutronUserAddress, - }, - ], - unbonded_amount: '200000', - withdrawed_amount: null, - }); - }); - - it('withdraw with non funded withdrawal manager', async () => { - const { - withdrawalVoucherContractClient: voucherContractClient, - neutronUserAddress, - } = context; - const tokenId = `0_${neutronUserAddress}_1`; - await expect( - voucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }), - ).rejects.toThrowError(/spendable balance {2}is smaller/); - }); - - it('fund withdrawal manager', async () => { - const { - withdrawalManagerContractClient, - neutronUserAddress, - neutronIBCDenom, - } = context; - const res = await context.client.sendTokens( - neutronUserAddress, - withdrawalManagerContractClient.contractAddress, - [{ amount: '200000', denom: neutronIBCDenom }], - 1.6, - undefined, - ); - expect(res.code).toEqual(0); - }); - - it('withdraw', async () => { - const { - withdrawalVoucherContractClient: voucherContractClient, - neutronUserAddress, - neutronClient, - neutronIBCDenom, - } = context; - const balanceBefore = parseInt( - ( - await neutronClient.CosmosBankV1Beta1.query.queryBalance( + 1.5, + undefined, + [{ amount: '20000', denom: 'untrn' }], + ); + await waitFor(async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.withdrawalManagerContractClient.contractAddress, + ); + return balances.data.balances.length > 0; + }, 20_000); + }); + it('withdraw', async () => { + const { + withdrawalVoucherContractClient: voucherContractClient, + neutronUserAddress, + neutronClient, + neutronIBCDenom, + } = context; + const balanceBefore = parseInt( + ( + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronUserAddress, + { denom: neutronIBCDenom }, + ) + ).data.balance.amount, + ); + const tokenId = `0_${neutronUserAddress}_1`; + const res = await voucherContractClient.sendNft(neutronUserAddress, { + token_id: tokenId, + contract: context.withdrawalManagerContractClient.contractAddress, + msg: Buffer.from( + JSON.stringify({ + withdraw: {}, + }), + ).toString('base64'), + }); + expect(res.transactionHash).toHaveLength(64); + const balance = + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronUserAddress, + { denom: neutronIBCDenom }, + ); + expect(parseInt(balance.data.balance.amount) - balanceBefore).toBe( + 200000, + ); + }); + it('withdraw to custom receiver', async () => { + const { + withdrawalVoucherContractClient: voucherContractClient, neutronUserAddress, - { denom: neutronIBCDenom }, - ) - ).data.balance.amount, - ); - const tokenId = `0_${neutronUserAddress}_1`; - const res = await voucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }); - expect(res.transactionHash).toHaveLength(64); - const balance = await neutronClient.CosmosBankV1Beta1.query.queryBalance( - neutronUserAddress, - { denom: neutronIBCDenom }, - ); - expect(parseInt(balance.data.balance.amount) - balanceBefore).toBe(150000); - }); - it('withdraw to custom receiver', async () => { - const { - withdrawalVoucherContractClient: voucherContractClient, - neutronUserAddress, - neutronSecondUserAddress, - neutronClient, - neutronIBCDenom, - } = context; - const balanceBefore = parseInt( - ( - await neutronClient.CosmosBankV1Beta1.query.queryBalance( neutronSecondUserAddress, - { denom: neutronIBCDenom }, - ) - ).data.balance.amount, - ); - expect(balanceBefore).toEqual(0); - const tokenId = `0_${neutronUserAddress}_2`; - const res = await voucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: { - receiver: neutronSecondUserAddress, - }, - }), - ).toString('base64'), + neutronClient, + neutronIBCDenom, + } = context; + const balanceBefore = parseInt( + ( + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronSecondUserAddress, + { denom: neutronIBCDenom }, + ) + ).data.balance.amount, + ); + expect(balanceBefore).toEqual(0); + const tokenId = `0_${neutronUserAddress}_2`; + const res = await voucherContractClient.sendNft(neutronUserAddress, { + token_id: tokenId, + contract: context.withdrawalManagerContractClient.contractAddress, + msg: Buffer.from( + JSON.stringify({ + withdraw: { + receiver: neutronSecondUserAddress, + }, + }), + ).toString('base64'), + }); + expect(res.transactionHash).toHaveLength(64); + const balance = + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronSecondUserAddress, + { denom: neutronIBCDenom }, + ); + expect(parseInt(balance.data.balance.amount)).toBe(300000); + }); }); - expect(res.transactionHash).toHaveLength(64); - const balance = await neutronClient.CosmosBankV1Beta1.query.queryBalance( - neutronSecondUserAddress, - { denom: neutronIBCDenom }, - ); - expect(parseInt(balance.data.balance.amount)).toBe(50000); }); }); diff --git a/packages/base/src/msg/core.rs b/packages/base/src/msg/core.rs index 4d7ccbb4..186f2ef8 100644 --- a/packages/base/src/msg/core.rs +++ b/packages/base/src/msg/core.rs @@ -46,21 +46,11 @@ pub enum QueryMsg { #[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { - Bond { - receiver: Option, - }, + Bond { receiver: Option }, Unbond {}, //permissioned - UpdateConfig { - new_config: Box, - }, - UpdateNonNativeRewardsReceivers { - items: Vec, - }, - FakeProcessBatch { - batch_id: Uint128, - unbonded_amount: Uint128, - }, + UpdateConfig { new_config: Box }, + UpdateNonNativeRewardsReceivers { items: Vec }, Tick {}, PuppeteerHook(Box), } diff --git a/packages/base/src/msg/puppeteer.rs b/packages/base/src/msg/puppeteer.rs index 97644f30..70579612 100644 --- a/packages/base/src/msg/puppeteer.rs +++ b/packages/base/src/msg/puppeteer.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{from_json, Addr, Decimal, Uint128}; +use cosmwasm_std::{from_json, Addr, Decimal, Timestamp, Uint128}; use std::ops::Div; use std::str::FromStr; @@ -119,9 +119,10 @@ impl ExecuteMsg { #[cw_serde] pub struct MigrateMsg {} -pub type DelegationsResponse = (Delegations, u64); +pub type Height = u64; -pub type BalancesResponse = (Balances, u64); +pub type DelegationsResponse = (Delegations, Height, Timestamp); +pub type BalancesResponse = (Balances, Height, Timestamp); #[cw_serde] pub struct FeesResponse { diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index c8929e88..c934615e 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, Uint128}; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use lido_helpers::fsm::{Fsm, Transition}; use optfield::optfield; @@ -39,12 +39,14 @@ pub struct UnbondItem { } #[cw_serde] +#[derive(Copy)] pub enum UnbondBatchStatus { New, UnbondRequested, UnbondFailed, Unbonding, Unbonded, + Withdrawing, Withdrawn, } @@ -64,7 +66,26 @@ pub struct UnbondBatch { pub created: u64, } -pub const UNBOND_BATCHES: Map = Map::new("batches"); +pub struct UnbondBatchIndexes<'a> { + pub status: MultiIndex<'a, u8, UnbondBatch, u128>, +} + +impl<'a> IndexList for UnbondBatchIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.status]; + Box::new(v.into_iter()) + } +} + +pub fn unbond_batches_map<'a>() -> IndexedMap<'a, u128, UnbondBatch, UnbondBatchIndexes<'a>> { + IndexedMap::new( + "batches", + UnbondBatchIndexes { + status: MultiIndex::new(|_pk, b| b.status as u8, "batches", "batches__status"), + }, + ) +} + pub const UNBOND_BATCH_ID: Item = Item::new("batches_ids"); pub const TOTAL_LSM_SHARES: Item = Item::new("total_lsm_shares"); diff --git a/packages/base/src/state/puppeteer.rs b/packages/base/src/state/puppeteer.rs index c583e27e..774db5e8 100644 --- a/packages/base/src/state/puppeteer.rs +++ b/packages/base/src/state/puppeteer.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, Timestamp}; use cw_storage_plus::Item; use lido_puppeteer_base::state::BaseConfig; @@ -38,8 +38,8 @@ pub enum KVQueryType { NonNativeRewardsBalances, } -pub const NON_NATIVE_REWARD_BALANCES: Item<(MultiBalances, u64)> = +pub const NON_NATIVE_REWARD_BALANCES: Item<(MultiBalances, u64, Timestamp)> = Item::new("non_native_reward_balances"); -pub const DELEGATIONS_AND_BALANCE: Item<(BalancesAndDelegations, u64)> = +pub const DELEGATIONS_AND_BALANCE: Item<(BalancesAndDelegations, u64, Timestamp)> = Item::new("delegations_and_balance"); diff --git a/packages/puppeteer-base/src/proto.rs b/packages/puppeteer-base/src/proto.rs index c6fabe33..6e80d7e2 100644 --- a/packages/puppeteer-base/src/proto.rs +++ b/packages/puppeteer-base/src/proto.rs @@ -6,6 +6,7 @@ pub struct MsgDelegateResponse {} #[cw_serde] pub struct MsgIBCTransfer {} + #[cw_serde] pub struct MsgSendResponse {} diff --git a/packages/puppeteer-base/src/sudo.rs b/packages/puppeteer-base/src/sudo.rs index 7c274f71..a4e02bdd 100644 --- a/packages/puppeteer-base/src/sudo.rs +++ b/packages/puppeteer-base/src/sudo.rs @@ -6,7 +6,7 @@ use cosmos_sdk_proto::cosmos::{ bank::v1beta1::MsgSend, tx::v1beta1::{TxBody, TxRaw}, }; -use cosmwasm_std::{Binary, DepsMut, Env, Response, StdError}; +use cosmwasm_std::{Binary, DepsMut, Env, Response, StdError, Timestamp}; use cw_storage_plus::Index; use neutron_sdk::{ bindings::{ @@ -102,11 +102,12 @@ where deps: DepsMut, env: Env, query_id: u64, - storage: cw_storage_plus::Item<'a, (X, u64)>, + storage: cw_storage_plus::Item<'a, (X, u64, Timestamp)>, ) -> NeutronResult { let data = query_kv_result(deps.as_ref(), query_id)?; let height = env.block.height; - storage.save(deps.storage, &(data, height))?; + let timestamp = env.block.time; + storage.save(deps.storage, &(data, height, timestamp))?; Ok(Response::default()) } From f89dfde639333c15e0bd73fde99d09973c153396 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 11 Mar 2024 14:34:18 +0400 Subject: [PATCH 2/2] chore: meh --- integration_tests/dockerfiles/gaia/build.sh | 31 ++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/integration_tests/dockerfiles/gaia/build.sh b/integration_tests/dockerfiles/gaia/build.sh index 20a92461..77c67d52 100755 --- a/integration_tests/dockerfiles/gaia/build.sh +++ b/integration_tests/dockerfiles/gaia/build.sh @@ -2,27 +2,26 @@ DIR="$(dirname $0)" cd $DIR VERSION=$(cat ../../package.json | jq -r '.version') +git clone https://github.com/cosmos/gaia.git -b v14.1.0 +cp ./Dockerfile ./gaia if [[ "$CI" == "true" ]]; then VERSION="_$VERSION" ORG=neutronorg/lionco-contracts: else VERSION=":$VERSION" + new_replace="github.com/cosmos/ibc-go/v4 v4.4.2 => github.com/ratik/ibc-go/v4 v4.4.3-0.20231115171220-5c22b66cfa8c" + gomod_file="gaia/go.mod" + cp "$gomod_file" "$gomod_file.bak" + awk -v new_replace="$new_replace" ' + BEGIN { replace_block=0; added=0 } + /replace[[:space:]]*\(/ { replace_block=1 } + /^[[:space:]]*\)/ { if(replace_block) { print new_replace; added=1; replace_block=0 } } + { print } + END { if(!added) { print "replace ("; print new_replace; print ")" } } + ' "$gomod_file.bak" > "$gomod_file" + cd gaia + go mod tidy + cd .. fi -git clone https://github.com/cosmos/gaia.git -b v14.1.0 -cp ./Dockerfile ./gaia - -new_replace="github.com/cosmos/ibc-go/v4 v4.4.2 => github.com/ratik/ibc-go/v4 v4.4.3-0.20231115171220-5c22b66cfa8c" -gomod_file="gaia/go.mod" -cp "$gomod_file" "$gomod_file.bak" -awk -v new_replace="$new_replace" ' -BEGIN { replace_block=0; added=0 } -/replace[[:space:]]*\(/ { replace_block=1 } -/^[[:space:]]*\)/ { if(replace_block) { print new_replace; added=1; replace_block=0 } } -{ print } -END { if(!added) { print "replace ("; print new_replace; print ")" } } -' "$gomod_file.bak" > "$gomod_file" -cd gaia -go mod tidy -cd .. docker build gaia -t ${ORG}gaia-test${VERSION} rm -rf ./gaia \ No newline at end of file