From 0ccae9ce9c00189bf979c333fd60aa971924a769 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 28 Feb 2024 21:49:29 +0100 Subject: [PATCH 1/7] feat: transfer shares to the remote chain --- contracts/core/src/contract.rs | 90 ++++++- contracts/factory/src/contract.rs | 1 + contracts/factory/src/msg.rs | 1 + .../src/generated/contractLib/lidoCore.ts | 23 +- .../src/generated/contractLib/lidoFactory.ts | 3 +- .../contractLib/lidoPuppeteerAuthz.ts | 51 +--- .../src/testcases/core.fsm.test.ts | 228 ++++++++++++++++++ packages/base/src/msg/core.rs | 7 +- packages/base/src/state/core.rs | 8 +- 9 files changed, 342 insertions(+), 70 deletions(-) diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index 1cb00c1d..ae8ecfa4 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -1,9 +1,9 @@ use crate::error::{ContractError, ContractResult}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - attr, ensure, ensure_eq, ensure_ne, entry_point, to_json_binary, Attribute, BankQuery, Binary, - Coin, CosmosMsg, CustomQuery, Decimal, Deps, DepsMut, Env, MessageInfo, QueryRequest, Response, - StdError, StdResult, Timestamp, Uint128, WasmMsg, + attr, ensure, ensure_eq, ensure_ne, entry_point, to_json_binary, Addr, Attribute, BankQuery, + Binary, Coin, CosmosMsg, CustomQuery, Decimal, Deps, DepsMut, Env, MessageInfo, QueryRequest, + Response, StdError, StdResult, Timestamp, Uint128, WasmMsg, }; use cw2::set_contract_version; use lido_helpers::answer::response; @@ -11,8 +11,8 @@ use lido_puppeteer_base::msg::TransferReadyBatchMsg; use lido_staking_base::state::core::{ Config, ConfigOptional, ContractState, NonNativeRewardsItem, UnbondBatch, UnbondBatchStatus, UnbondItem, 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, + LAST_PUPPETEER_RESPONSE, LSM_SHARES_TO_REDEEM, NON_NATIVE_REWARDS_CONFIG, PENDING_LSM_SHARES, + PENDING_TRANSFER, PRE_UNBONDING_BALANCE, TOTAL_LSM_SHARES, UNBOND_BATCHES, UNBOND_BATCH_ID, }; use lido_staking_base::state::validatorset::ValidatorInfo; use lido_staking_base::state::withdrawal_voucher::{Metadata, Trait}; @@ -56,6 +56,8 @@ pub fn instantiate( LAST_IDLE_CALL.save(deps.storage, &0)?; LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &0)?; TOTAL_LSM_SHARES.save(deps.storage, &0)?; + LSM_SHARES_TO_REDEEM.save(deps.storage, &vec![])?; + PENDING_LSM_SHARES.save(deps.storage, &vec![])?; Ok(response("instantiate", CONTRACT_NAME, attrs)) } @@ -63,6 +65,13 @@ pub fn instantiate( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { Ok(match msg { QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?)?, + QueryMsg::Owner {} => to_json_binary( + &cw_ownable::get_ownership(deps.storage)? + .owner + .unwrap_or(Addr::unchecked("")) + .to_string(), + )?, + QueryMsg::PendingLSMShares {} => query_pending_lsm_shares(deps)?, QueryMsg::ExchangeRate {} => to_json_binary(&query_exchange_rate(deps, env, None)?)?, QueryMsg::UnbondBatch { batch_id } => query_unbond_batch(deps, batch_id)?, QueryMsg::NonNativeRewardsReceivers {} => { @@ -75,6 +84,11 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResul }) } +fn query_pending_lsm_shares(deps: Deps) -> ContractResult { + let shares: Vec<(String, Uint128)> = PENDING_LSM_SHARES.load(deps.storage)?; + to_json_binary(&shares).map_err(From::from) +} + fn query_exchange_rate( deps: Deps, env: Env, @@ -211,6 +225,19 @@ fn execute_puppeteer_hook( ); if let lido_puppeteer_base::msg::ResponseHookMsg::Success(_) = msg { LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &env.block.height)?; + if let lido_puppeteer_base::msg::ResponseHookMsg::Success(success_msg) = &msg { + if let lido_puppeteer_base::msg::Transaction::IBCTransfer { + denom, + amount, + recipient: _, + } = &success_msg.transaction + { + LSM_SHARES_TO_REDEEM.update(deps.storage, |mut arr| { + arr.push((denom.to_string(), Uint128::from(*amount))); + StdResult::Ok(arr) + })?; + } + } } // if it's error we don't need to save the height because balance wasn't changed LAST_PUPPETEER_RESPONSE.save(deps.storage, &msg)?; @@ -248,8 +275,14 @@ fn execute_tick_idle( 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) = get_non_native_rewards_transfer_msg(deps.as_ref(), info, env)? { + if let Some(transfer_msg) = + get_non_native_rewards_transfer_msg(deps.as_ref(), info.clone(), &env)? + { messages.push(transfer_msg); + } else if let Some(lsm_msg) = + get_pending_lsm_share_msg(deps, config, &env, info.funds.clone())? + { + messages.push(lsm_msg); } else { //return error if none return Err(ContractError::IdleMinIntervalIsNotReached {}); @@ -308,7 +341,7 @@ fn execute_tick_idle( if validators_to_claim.is_empty() { attrs.push(attr("validators_to_claim", "empty")); if let Some((transfer_msg, pending_amount)) = - get_transfer_pending_balance(deps.as_ref(), &env, config, info.funds.clone())? + get_transfer_pending_balance_msg(deps.as_ref(), &env, config, info.funds.clone())? { FSM.go_to(deps.storage, ContractState::Transfering)?; PENDING_TRANSFER.save(deps.storage, &pending_amount)?; @@ -371,7 +404,7 @@ fn execute_tick_claiming( } } if let Some((transfer_msg, pending_amount)) = - get_transfer_pending_balance(deps.as_ref(), &env, config, info.funds.clone())? + get_transfer_pending_balance_msg(deps.as_ref(), &env, config, info.funds.clone())? { FSM.go_to(deps.storage, ContractState::Transfering)?; PENDING_TRANSFER.save(deps.storage, &pending_amount)?; @@ -535,6 +568,10 @@ fn execute_bond( if denom_type == check_denom::DenomType::LsmShare { TOTAL_LSM_SHARES.update(deps.storage, |total| StdResult::Ok(total + amount.u128()))?; + PENDING_LSM_SHARES.update(deps.storage, |mut arr| { + arr.push((denom, amount)); + StdResult::Ok(arr) + })?; } let mut attrs = vec![attr("action", "bond")]; @@ -746,7 +783,7 @@ fn get_unbonded_batch(deps: Deps) -> ContractResult( +fn get_transfer_pending_balance_msg( deps: Deps, env: &Env, config: &Config, @@ -898,7 +935,7 @@ fn new_unbond(now: u64) -> lido_staking_base::state::core::UnbondBatch { fn get_non_native_rewards_transfer_msg( deps: Deps, info: MessageInfo, - env: Env, + env: &Env, ) -> ContractResult>> { let config = CONFIG.load(deps.storage)?; let non_native_rewards_receivers = NON_NATIVE_REWARDS_CONFIG.load(deps.storage)?; @@ -943,6 +980,39 @@ fn get_non_native_rewards_transfer_msg( }))) } +fn get_pending_lsm_share_msg( + deps: DepsMut, + config: &Config, + env: &Env, + funds: Vec, +) -> ContractResult>> { + let mut lsm_share: Option<(String, Uint128)> = None; + PENDING_LSM_SHARES.update(deps.storage, |mut arr| { + if arr.is_empty() { + return StdResult::Ok(arr); + } + lsm_share = Some(arr.remove(0)); + StdResult::Ok(arr) + })?; + match lsm_share { + Some((denom, amount)) => Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.puppeteer_contract.to_string(), + msg: to_json_binary( + &lido_staking_base::msg::puppeteer::ExecuteMsg::IBCTransfer { + timeout: config.puppeteer_timeout, + reply_to: env.contract.address.to_string(), + }, + )?, + funds: { + let mut all_funds = vec![cosmwasm_std::Coin { denom, amount }]; + all_funds.extend(funds); + all_funds + }, + }))), + None => Ok(None), + } +} + mod check_denom { use super::*; diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs index 9c76b4f1..22684ffc 100644 --- a/contracts/factory/src/contract.rs +++ b/contracts/factory/src/contract.rs @@ -426,6 +426,7 @@ fn execute_init( unbond_batch_switch_time: core_params.unbond_batch_switch_time, idle_min_interval: core_params.idle_min_interval, channel: core_params.channel, + lsm_redeem_threshold: core_params.lsm_redeem_threshold, owner: env.contract.address.to_string(), })?, funds: vec![], diff --git a/contracts/factory/src/msg.rs b/contracts/factory/src/msg.rs index e681cc26..c90bb67c 100644 --- a/contracts/factory/src/msg.rs +++ b/contracts/factory/src/msg.rs @@ -24,6 +24,7 @@ pub struct CoreParams { pub unbonding_period: u64, pub unbonding_safe_period: u64, pub unbond_batch_switch_time: u64, + pub lsm_redeem_threshold: u64, pub channel: String, } diff --git a/integration_tests/src/generated/contractLib/lidoCore.ts b/integration_tests/src/generated/contractLib/lidoCore.ts index dee528e3..b3d5ad65 100644 --- a/integration_tests/src/generated/contractLib/lidoCore.ts +++ b/integration_tests/src/generated/contractLib/lidoCore.ts @@ -4,6 +4,7 @@ export interface InstantiateMsg { base_denom: string; channel: string; idle_min_interval: number; + lsm_redeem_threshold: number; owner: string; pump_address?: string | null; puppeteer_contract: string; @@ -149,6 +150,8 @@ export type Transaction = }; }; export type ArrayOfNonNativeRewardsItem = NonNativeRewardsItem[]; +export type String = string; +export type ArrayOfTupleOf_StringAnd_Uint128 = [string, Uint128][]; /** * A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0 * @@ -214,7 +217,15 @@ export type Timestamp2 = Uint64; export type Uint64 = string; export interface LidoCoreSchema { - responses: Config | ContractState | Decimal | ResponseHookMsg | ArrayOfNonNativeRewardsItem | UnbondBatch; + responses: + | Config + | ContractState + | Decimal + | ResponseHookMsg + | ArrayOfNonNativeRewardsItem + | String + | ArrayOfTupleOf_StringAnd_Uint128 + | UnbondBatch; query: UnbondBatchArgs; execute: | BondArgs @@ -230,7 +241,7 @@ export interface Config { channel: string; idle_min_interval: number; ld_denom?: string | null; - owner: string; + lsm_redeem_threshold: number; pump_address?: string | null; puppeteer_contract: string; puppeteer_timeout: number; @@ -339,7 +350,7 @@ export interface ConfigOptional { channel?: string | null; idle_min_interval?: number | null; ld_denom?: string | null; - owner?: string | null; + lsm_redeem_threshold?: number | null; pump_address?: string | null; puppeteer_contract?: string | null; puppeteer_timeout?: number | null; @@ -395,6 +406,9 @@ export class Client { queryConfig = async(): Promise => { return this.client.queryContractSmart(this.contractAddress, { config: {} }); } + queryOwner = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { owner: {} }); + } queryExchangeRate = async(): Promise => { return this.client.queryContractSmart(this.contractAddress, { exchange_rate: {} }); } @@ -410,6 +424,9 @@ export class Client { queryNonNativeRewardsReceivers = async(): Promise => { return this.client.queryContractSmart(this.contractAddress, { non_native_rewards_receivers: {} }); } + queryPendingLSMShares = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { pending_l_s_m_shares: {} }); + } 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); diff --git a/integration_tests/src/generated/contractLib/lidoFactory.ts b/integration_tests/src/generated/contractLib/lidoFactory.ts index 24c151d1..04d53574 100644 --- a/integration_tests/src/generated/contractLib/lidoFactory.ts +++ b/integration_tests/src/generated/contractLib/lidoFactory.ts @@ -146,6 +146,7 @@ export interface InitArgs { export interface CoreParams { channel: string; idle_min_interval: number; + lsm_redeem_threshold: number; puppeteer_timeout: number; unbond_batch_switch_time: number; unbonding_period: number; @@ -156,7 +157,7 @@ export interface ConfigOptional { channel?: string | null; idle_min_interval?: number | null; ld_denom?: string | null; - owner?: string | null; + lsm_redeem_threshold?: number | null; pump_address?: string | null; puppeteer_contract?: string | null; puppeteer_timeout?: number | null; diff --git a/integration_tests/src/generated/contractLib/lidoPuppeteerAuthz.ts b/integration_tests/src/generated/contractLib/lidoPuppeteerAuthz.ts index a2cc8622..4a5e12aa 100644 --- a/integration_tests/src/generated/contractLib/lidoPuppeteerAuthz.ts +++ b/integration_tests/src/generated/contractLib/lidoPuppeteerAuthz.ts @@ -34,37 +34,11 @@ export type Addr = string; export type Uint128 = string; export type IcaState = "none" | "in_progress" | "registered" | "timeout"; export type ArrayOfTransfer = Transfer[]; -/** - * A point in time in nanosecond precision. - * - * This type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z. - * - * ## Examples - * - * ``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202); - * - * let ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ``` - */ -export type Timestamp = Uint64; -/** - * A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. - * - * # Examples - * - * Use `from` to create instances of this and `u64` to get the value out: - * - * ``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42); - * - * let b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ``` - */ -export type Uint64 = string; -export type ArrayOfUnbondingDelegation = UnbondingDelegation[]; export interface LidoPuppeteerAuthzSchema { - responses: Config | DelegationsResponse | State | ArrayOfTransfer | ArrayOfUnbondingDelegation; + responses: Config | DelegationsResponse | State | ArrayOfTransfer; execute: | RegisterDelegatorDelegationsQueryArgs - | RegisterDelegatorUnbondingDelegationsQueryArgs | SetFeesArgs | DelegateArgs | UndelegateArgs @@ -119,25 +93,9 @@ export interface Transfer { recipient: string; sender: string; } -export interface UnbondingDelegation { - last_updated_height: number; - query_id: number; - unbonding_delegations: UnbondingEntry[]; - validator_address: string; -} -export interface UnbondingEntry { - balance: Uint128; - completion_time?: Timestamp | null; - creation_height: number; - initial_balance: Uint128; - [k: string]: unknown; -} export interface RegisterDelegatorDelegationsQueryArgs { validators: string[]; } -export interface RegisterDelegatorUnbondingDelegationsQueryArgs { - validators: string[]; -} export interface SetFeesArgs { ack_fee: Uint128; recv_fee: Uint128; @@ -220,9 +178,6 @@ export class Client { queryDelegations = async(): Promise => { return this.client.queryContractSmart(this.contractAddress, { delegations: {} }); } - queryUnbondingDelegations = async(): Promise => { - return this.client.queryContractSmart(this.contractAddress, { unbonding_delegations: {} }); - } registerICA = 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, { register_i_c_a: {} }, fee || "auto", memo, funds); @@ -235,10 +190,6 @@ export class Client { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { register_delegator_delegations_query: args }, fee || "auto", memo, funds); } - registerDelegatorUnbondingDelegationsQuery = async(sender:string, args: RegisterDelegatorUnbondingDelegationsQueryArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { - if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } - return this.client.execute(sender, this.contractAddress, { register_delegator_unbonding_delegations_query: args }, fee || "auto", memo, funds); - } setFees = async(sender:string, args: SetFeesArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { set_fees: args }, fee || "auto", memo, funds); diff --git a/integration_tests/src/testcases/core.fsm.test.ts b/integration_tests/src/testcases/core.fsm.test.ts index d990516c..ee22b99c 100644 --- a/integration_tests/src/testcases/core.fsm.test.ts +++ b/integration_tests/src/testcases/core.fsm.test.ts @@ -15,6 +15,7 @@ import { setupStakingExtension, setupBankExtension, SigningStargateClient, + IndexedTx, } from '@cosmjs/stargate'; import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx'; import { join } from 'path'; @@ -377,6 +378,7 @@ describe('Core', () => { unbonding_safe_period: 10, unbonding_period: 60, channel: 'channel-0', + lsm_redeem_threshold: 2, }, }); expect(res.transactionHash).toHaveLength(64); @@ -1187,5 +1189,231 @@ describe('Core', () => { ).rejects.toThrowError(/Idle min interval is not reached/); }); }); + describe('fourth cycle', () => { + let lsmDenoms: string[] = []; + it('create LSM shares and send them to neutron', async () => { + const oldBalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + const oldBalanceDenoms = oldBalances.data.balances.map((b) => b.denom); + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.validatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.secondValidatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.validatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find((a) => a.denom == `${context.validatorAddress}/1`), + ).toEqual({ + denom: `${context.validatorAddress}/1`, + amount: '60000', + }); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.secondValidatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find( + (a) => a.denom == `${context.secondValidatorAddress}/2`, + ), + ).toEqual({ + denom: `${context.secondValidatorAddress}/2`, + amount: '60000', + }); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.validatorAddress}/1 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.secondValidatorAddress}/2 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + await waitFor(async () => { + const newbalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + const newDenoms = newbalances.data.balances.map((b) => b.denom); + const diff = newDenoms.filter((d) => !oldBalanceDenoms.includes(d)); + lsmDenoms = diff; + return diff.length === 2; + }, 30_000); + } + }); + it('bond LSM shares', async () => { + { + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '60000', + denom: lsmDenoms[0], + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + } + { + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '60000', + denom: lsmDenoms[1], + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + } + }); + it('verify pending lsm shares', async () => { + const pending = + await context.coreContractClient.queryPendingLSMShares(); + expect(pending).toEqual([ + [lsmDenoms[0], '60000'], + [lsmDenoms[1], '60000'], + ]); + }); + 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('lsm shares are not on contract balance', async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.coreContractClient.contractAddress, + ); + console.log(lsmDenoms); + console.log(balances.data.balances); + expect( + balances.data.balances.find((one) => one.denom === lsmDenoms[0]), + ).toBeFalsy(); + }); + it('await for puppeteer response', async () => { + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 30_000); + }); + it('verify pending lsm shares', async () => { + const pending = + await context.coreContractClient.queryPendingLSMShares(); + expect(pending).toEqual([[lsmDenoms[1], '60000']]); + }); + }); }); }); diff --git a/packages/base/src/msg/core.rs b/packages/base/src/msg/core.rs index 11dcbbc5..fad66c09 100644 --- a/packages/base/src/msg/core.rs +++ b/packages/base/src/msg/core.rs @@ -15,6 +15,7 @@ pub struct InstantiateMsg { pub validators_set_contract: String, pub base_denom: String, pub remote_denom: String, + pub lsm_redeem_threshold: u64, pub idle_min_interval: u64, //seconds pub unbonding_period: u64, //seconds pub unbonding_safe_period: u64, //seconds @@ -29,6 +30,8 @@ pub struct InstantiateMsg { pub enum QueryMsg { #[returns(Config)] Config {}, + #[returns(String)] + Owner {}, #[returns(cosmwasm_std::Decimal)] ExchangeRate {}, #[returns(crate::state::core::UnbondBatch)] @@ -39,6 +42,8 @@ pub enum QueryMsg { LastPuppeteerResponse {}, #[returns(Vec)] NonNativeRewardsReceivers {}, + #[returns(Vec<(String,Uint128)>)] + PendingLSMShares {}, } #[cw_ownable_execute] @@ -78,12 +83,12 @@ impl From for Config { base_denom: val.base_denom, remote_denom: val.remote_denom, channel: val.channel, - owner: val.owner, ld_denom: None, idle_min_interval: val.idle_min_interval, unbonding_safe_period: val.unbonding_safe_period, unbonding_period: val.unbonding_period, pump_address: val.pump_address, + lsm_redeem_threshold: val.lsm_redeem_threshold, validators_set_contract: val.validators_set_contract, unbond_batch_switch_time: val.unbond_batch_switch_time, } diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index 215c5af7..792354b1 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -22,9 +22,9 @@ pub struct Config { pub unbonding_safe_period: u64, //seconds pub unbond_batch_switch_time: u64, //seconds pub pump_address: Option, - pub owner: String, pub channel: String, pub ld_denom: Option, + pub lsm_redeem_threshold: u64, } pub const CONFIG: Item = Item::new("config"); @@ -65,6 +65,8 @@ pub struct UnbondBatch { pub const UNBOND_BATCHES: Map = Map::new("batches"); pub const UNBOND_BATCH_ID: Item = Item::new("batches_ids"); pub const TOTAL_LSM_SHARES: Item = Item::new("total_lsm_shares"); +pub const PENDING_LSM_SHARES: Item> = Item::new("pending_lsm_shares"); +pub const LSM_SHARES_TO_REDEEM: Item> = Item::new("lsm_shares_to_redeem"); #[cw_serde] pub enum ContractState { @@ -88,10 +90,6 @@ const TRANSITIONS: &[Transition] = &[ from: ContractState::Idle, to: ContractState::Transfering, }, - Transition { - from: ContractState::Idle, - to: ContractState::Claiming, - }, Transition { from: ContractState::Claiming, to: ContractState::Transfering, From ec785d8f9b8102d18cdcd5762f5a06055692df61 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Thu, 29 Feb 2024 00:32:56 +0100 Subject: [PATCH 2/7] feat: fml --- contracts/core/src/contract.rs | 60 ++-- .../src/testcases/core.fsm.test.ts | 294 +++++++++--------- packages/base/src/state/core.rs | 4 +- 3 files changed, 193 insertions(+), 165 deletions(-) diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index ae8ecfa4..960b9f97 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -56,8 +56,6 @@ pub fn instantiate( LAST_IDLE_CALL.save(deps.storage, &0)?; LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &0)?; TOTAL_LSM_SHARES.save(deps.storage, &0)?; - LSM_SHARES_TO_REDEEM.save(deps.storage, &vec![])?; - PENDING_LSM_SHARES.save(deps.storage, &vec![])?; Ok(response("instantiate", CONTRACT_NAME, attrs)) } @@ -85,7 +83,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResul } fn query_pending_lsm_shares(deps: Deps) -> ContractResult { - let shares: Vec<(String, Uint128)> = PENDING_LSM_SHARES.load(deps.storage)?; + let shares: Vec<(String, Uint128)> = PENDING_LSM_SHARES + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::>>()?; to_json_binary(&shares).map_err(From::from) } @@ -223,19 +223,51 @@ fn execute_puppeteer_hook( config.puppeteer_contract, ContractError::Unauthorized {} ); + deps.api + .debug(&format!("WASMDEBUG: puppeteer_hook: {:?}", msg)); if let lido_puppeteer_base::msg::ResponseHookMsg::Success(_) = msg { LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &env.block.height)?; + deps.api.debug("height saved"); if let lido_puppeteer_base::msg::ResponseHookMsg::Success(success_msg) = &msg { + deps.api.debug(&format!( + "WASMDEBUG: transaction: {:?}", + success_msg.transaction + )); if let lido_puppeteer_base::msg::Transaction::IBCTransfer { denom, amount, recipient: _, } = &success_msg.transaction { - LSM_SHARES_TO_REDEEM.update(deps.storage, |mut arr| { - arr.push((denom.to_string(), Uint128::from(*amount))); - StdResult::Ok(arr) - })?; + deps.api.debug(&format!("WASMDEBUG: denom: {:?}", denom)); + deps.api.debug(&format!( + "WASMDEBUG: keys: {:?}", + PENDING_LSM_SHARES + .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::>>()? + )); + let current_amount = + PENDING_LSM_SHARES.may_load(deps.storage, denom.to_string())?; + if let Some(current_amount) = current_amount { + deps.api + .debug(&format!("WASMDEBUG: current_amount: {:?}", current_amount)); + let sent_amount = Uint128::from(*amount); + LSM_SHARES_TO_REDEEM.update(deps.storage, denom.to_string(), |one| { + StdResult::Ok(one.unwrap_or(Uint128::zero()) + sent_amount) + })?; + if current_amount == sent_amount { + PENDING_LSM_SHARES.remove(deps.storage, denom.to_string()); + } else { + PENDING_LSM_SHARES.update( + deps.storage, + denom.to_string(), + |one| match one { + Some(one) => StdResult::Ok(one - Uint128::from(*amount)), + None => unreachable!("denom should be in the map"), + }, + )?; + } + } } } } // if it's error we don't need to save the height because balance wasn't changed @@ -568,9 +600,8 @@ fn execute_bond( if denom_type == check_denom::DenomType::LsmShare { TOTAL_LSM_SHARES.update(deps.storage, |total| StdResult::Ok(total + amount.u128()))?; - PENDING_LSM_SHARES.update(deps.storage, |mut arr| { - arr.push((denom, amount)); - StdResult::Ok(arr) + PENDING_LSM_SHARES.update(deps.storage, denom, |one| { + StdResult::Ok(one.unwrap_or(Uint128::zero()) + amount) })?; } @@ -986,14 +1017,7 @@ fn get_pending_lsm_share_msg( env: &Env, funds: Vec, ) -> ContractResult>> { - let mut lsm_share: Option<(String, Uint128)> = None; - PENDING_LSM_SHARES.update(deps.storage, |mut arr| { - if arr.is_empty() { - return StdResult::Ok(arr); - } - lsm_share = Some(arr.remove(0)); - StdResult::Ok(arr) - })?; + let lsm_share: Option<(String, Uint128)> = PENDING_LSM_SHARES.first(deps.storage)?; match lsm_share { Some((denom, amount)) => Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.puppeteer_contract.to_string(), diff --git a/integration_tests/src/testcases/core.fsm.test.ts b/integration_tests/src/testcases/core.fsm.test.ts index ee22b99c..08a1b15b 100644 --- a/integration_tests/src/testcases/core.fsm.test.ts +++ b/integration_tests/src/testcases/core.fsm.test.ts @@ -1011,7 +1011,7 @@ describe('Core', () => { expect(state).toEqual('idle'); }); }); - describe('third cycle', () => { + describe('third cycle (non-native rewards)', () => { let remoteNonNativeDenoms: string[] = []; it('generate two new tokenfactory tokens and send them to the remote zone', async () => { const { neutronUserAddress } = context; @@ -1189,137 +1189,146 @@ describe('Core', () => { ).rejects.toThrowError(/Idle min interval is not reached/); }); }); - describe('fourth cycle', () => { + describe('fourth cycle (LSM-shares)', () => { let lsmDenoms: string[] = []; - it('create LSM shares and send them to neutron', async () => { - const oldBalances = - await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - context.neutronUserAddress, - ); - const oldBalanceDenoms = oldBalances.data.balances.map((b) => b.denom); - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking delegate ${context.validatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking delegate ${context.secondValidatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking tokenize-share ${context.validatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - const balances = await context.gaiaQueryClient.bank.allBalances( - context.gaiaUserAddress, - ); - expect( - balances.find((a) => a.denom == `${context.validatorAddress}/1`), - ).toEqual({ - denom: `${context.validatorAddress}/1`, - amount: '60000', - }); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking tokenize-share ${context.secondValidatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - const balances = await context.gaiaQueryClient.bank.allBalances( - context.gaiaUserAddress, - ); - expect( - balances.find( - (a) => a.denom == `${context.secondValidatorAddress}/2`, - ), - ).toEqual({ - denom: `${context.secondValidatorAddress}/2`, - amount: '60000', - }); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.validatorAddress}/1 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.secondValidatorAddress}/2 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } - { + let oldBalanceDenoms: string[] = []; + describe('create LSM shares and send them to neutron', () => { + it('get balances', async () => { + const oldBalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + oldBalanceDenoms = oldBalances.data.balances.map((b) => b.denom); + }); + it('delegate', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.validatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.secondValidatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + }); + it('tokenize shares', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.validatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find((a) => a.denom == `${context.validatorAddress}/1`), + ).toEqual({ + denom: `${context.validatorAddress}/1`, + amount: '60000', + }); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.secondValidatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find( + (a) => a.denom == `${context.secondValidatorAddress}/2`, + ), + ).toEqual({ + denom: `${context.secondValidatorAddress}/2`, + amount: '60000', + }); + } + }); + it('transfer shares to neutron', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.validatorAddress}/1 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.secondValidatorAddress}/2 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + }); + it('wait for balances to come', async () => { await waitFor(async () => { const newbalances = await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( @@ -1329,8 +1338,8 @@ describe('Core', () => { const diff = newDenoms.filter((d) => !oldBalanceDenoms.includes(d)); lsmDenoms = diff; return diff.length === 2; - }, 30_000); - } + }, 60_000); + }); }); it('bond LSM shares', async () => { { @@ -1391,27 +1400,22 @@ describe('Core', () => { await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( context.coreContractClient.contractAddress, ); - console.log(lsmDenoms); - console.log(balances.data.balances); expect( balances.data.balances.find((one) => one.denom === lsmDenoms[0]), ).toBeFalsy(); }); - it('await for puppeteer response', async () => { - let response; + it('await for pending length decrease', async () => { + let pending: any; await waitFor(async () => { try { - response = - await context.coreContractClient.queryLastPuppeteerResponse(); + const res = + await context.coreContractClient.queryPendingLSMShares(); + pending = res; } catch (e) { // } - return !!response; - }, 30_000); - }); - it('verify pending lsm shares', async () => { - const pending = - await context.coreContractClient.queryPendingLSMShares(); + return !!pending && pending.length === 1; + }, 60_000); expect(pending).toEqual([[lsmDenoms[1], '60000']]); }); }); diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index 792354b1..60757c51 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -65,8 +65,8 @@ pub struct UnbondBatch { pub const UNBOND_BATCHES: Map = Map::new("batches"); pub const UNBOND_BATCH_ID: Item = Item::new("batches_ids"); pub const TOTAL_LSM_SHARES: Item = Item::new("total_lsm_shares"); -pub const PENDING_LSM_SHARES: Item> = Item::new("pending_lsm_shares"); -pub const LSM_SHARES_TO_REDEEM: Item> = Item::new("lsm_shares_to_redeem"); +pub const PENDING_LSM_SHARES: Map = Map::new("pending_lsm_shares"); +pub const LSM_SHARES_TO_REDEEM: Map = Map::new("lsm_shares_to_redeem"); #[cw_serde] pub enum ContractState { From 70d058f5193488e6be9d495eb902e6fc69b49c4f Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Thu, 29 Feb 2024 09:54:07 +0100 Subject: [PATCH 3/7] chore: remove debug --- contracts/core/src/contract.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index 960b9f97..659fa933 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -223,34 +223,18 @@ fn execute_puppeteer_hook( config.puppeteer_contract, ContractError::Unauthorized {} ); - deps.api - .debug(&format!("WASMDEBUG: puppeteer_hook: {:?}", msg)); if let lido_puppeteer_base::msg::ResponseHookMsg::Success(_) = msg { LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &env.block.height)?; - deps.api.debug("height saved"); if let lido_puppeteer_base::msg::ResponseHookMsg::Success(success_msg) = &msg { - deps.api.debug(&format!( - "WASMDEBUG: transaction: {:?}", - success_msg.transaction - )); if let lido_puppeteer_base::msg::Transaction::IBCTransfer { denom, amount, recipient: _, } = &success_msg.transaction { - deps.api.debug(&format!("WASMDEBUG: denom: {:?}", denom)); - deps.api.debug(&format!( - "WASMDEBUG: keys: {:?}", - PENDING_LSM_SHARES - .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .collect::>>()? - )); let current_amount = PENDING_LSM_SHARES.may_load(deps.storage, denom.to_string())?; if let Some(current_amount) = current_amount { - deps.api - .debug(&format!("WASMDEBUG: current_amount: {:?}", current_amount)); let sent_amount = Uint128::from(*amount); LSM_SHARES_TO_REDEEM.update(deps.storage, denom.to_string(), |one| { StdResult::Ok(one.unwrap_or(Uint128::zero()) + sent_amount) From 54cd38ee651e74cb0637b2c3ae39970eefdb621b Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Thu, 29 Feb 2024 14:45:43 +0100 Subject: [PATCH 4/7] feat: redeem lsm --- contracts/core/src/contract.rs | 130 +++-- contracts/hook-tester/src/contract.rs | 15 +- contracts/puppeteer/src/contract.rs | 50 +- .../src/generated/contractLib/lidoCore.ts | 50 +- .../generated/contractLib/lidoHookTester.ts | 11 +- .../generated/contractLib/lidoPuppeteer.ts | 23 +- .../src/testcases/core.fsm.test.ts | 535 +++++++++++------- packages/base/src/msg/core.rs | 4 +- packages/base/src/msg/puppeteer.rs | 11 +- packages/base/src/state/core.rs | 4 +- packages/puppeteer-base/src/msg.rs | 17 +- packages/puppeteer-base/src/state.rs | 9 +- 12 files changed, 540 insertions(+), 319 deletions(-) diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index 659fa933..57339ff0 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -8,6 +8,7 @@ use cosmwasm_std::{ use cw2::set_contract_version; use lido_helpers::answer::response; use lido_puppeteer_base::msg::TransferReadyBatchMsg; +use lido_puppeteer_base::state::RedeemShareItem; use lido_staking_base::state::core::{ Config, ConfigOptional, ContractState, NonNativeRewardsItem, UnbondBatch, UnbondBatchStatus, UnbondItem, CONFIG, FAILED_BATCH_ID, FSM, LAST_ICA_BALANCE_CHANGE_HEIGHT, @@ -70,6 +71,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResul .to_string(), )?, QueryMsg::PendingLSMShares {} => query_pending_lsm_shares(deps)?, + QueryMsg::LSMSharesToRedeem {} => query_lsm_shares_to_redeem(deps)?, QueryMsg::ExchangeRate {} => to_json_binary(&query_exchange_rate(deps, env, None)?)?, QueryMsg::UnbondBatch { batch_id } => query_unbond_batch(deps, batch_id)?, QueryMsg::NonNativeRewardsReceivers {} => { @@ -83,7 +85,14 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResul } fn query_pending_lsm_shares(deps: Deps) -> ContractResult { - let shares: Vec<(String, Uint128)> = PENDING_LSM_SHARES + let shares: Vec<(String, (String, Uint128))> = PENDING_LSM_SHARES + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::>>()?; + to_json_binary(&shares).map_err(From::from) +} + +fn query_lsm_shares_to_redeem(deps: Deps) -> ContractResult { + let shares: Vec<(String, (String, Uint128))> = LSM_SHARES_TO_REDEEM .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) .collect::>>()?; to_json_binary(&shares).map_err(From::from) @@ -226,32 +235,46 @@ fn execute_puppeteer_hook( if let lido_puppeteer_base::msg::ResponseHookMsg::Success(_) = msg { LAST_ICA_BALANCE_CHANGE_HEIGHT.save(deps.storage, &env.block.height)?; if let lido_puppeteer_base::msg::ResponseHookMsg::Success(success_msg) = &msg { - if let lido_puppeteer_base::msg::Transaction::IBCTransfer { - denom, - amount, - recipient: _, - } = &success_msg.transaction - { - let current_amount = - PENDING_LSM_SHARES.may_load(deps.storage, denom.to_string())?; - if let Some(current_amount) = current_amount { - let sent_amount = Uint128::from(*amount); - LSM_SHARES_TO_REDEEM.update(deps.storage, denom.to_string(), |one| { - StdResult::Ok(one.unwrap_or(Uint128::zero()) + sent_amount) - })?; - if current_amount == sent_amount { - PENDING_LSM_SHARES.remove(deps.storage, denom.to_string()); - } else { - PENDING_LSM_SHARES.update( - deps.storage, - denom.to_string(), - |one| match one { - Some(one) => StdResult::Ok(one - Uint128::from(*amount)), - None => unreachable!("denom should be in the map"), - }, - )?; + match &success_msg.transaction { + lido_puppeteer_base::msg::Transaction::IBCTransfer { + denom, + amount, + recipient: _, + } => { + let current_pending = + PENDING_LSM_SHARES.may_load(deps.storage, denom.to_string())?; + if let Some((remote_denom, current_amount)) = current_pending { + let sent_amount = Uint128::from(*amount); + LSM_SHARES_TO_REDEEM.update(deps.storage, denom.to_string(), |one| { + let mut new = one.unwrap_or((remote_denom, Uint128::zero())); + new.1 += sent_amount; + StdResult::Ok(new) + })?; + if current_amount == sent_amount { + PENDING_LSM_SHARES.remove(deps.storage, denom.to_string()); + } else { + PENDING_LSM_SHARES.update(deps.storage, denom.to_string(), |one| { + match one { + Some(one) => { + let mut new = one; + new.1 -= Uint128::from(*amount); + StdResult::Ok(new) + } + None => unreachable!("denom should be in the map"), + } + })?; + } + } + } + lido_puppeteer_base::msg::Transaction::RedeemShares { items, .. } => { + let mut sum = 0u128; + for item in items { + sum += item.amount.u128(); + LSM_SHARES_TO_REDEEM.remove(deps.storage, item.local_denom.to_string()); } + TOTAL_LSM_SHARES.update(deps.storage, |one| StdResult::Ok(one - sum))?; } + _ => {} } } } // if it's error we don't need to save the height because balance wasn't changed @@ -295,6 +318,10 @@ fn execute_tick_idle( get_non_native_rewards_transfer_msg(deps.as_ref(), info.clone(), &env)? { messages.push(transfer_msg); + } else if let Some(lsm_msg) = + get_pending_redeem_msg(deps.as_ref(), config, &env, info.funds.clone())? + { + messages.push(lsm_msg); } else if let Some(lsm_msg) = get_pending_lsm_share_msg(deps, config, &env, info.funds.clone())? { @@ -582,10 +609,12 @@ fn execute_bond( let Coin { amount, denom } = cw_utils::one_coin(&info)?; let denom_type = check_denom::check_denom(&deps, &denom, &config)?; - if denom_type == check_denom::DenomType::LsmShare { + if let check_denom::DenomType::LsmShare(remote_denom) = denom_type { TOTAL_LSM_SHARES.update(deps.storage, |total| StdResult::Ok(total + amount.u128()))?; PENDING_LSM_SHARES.update(deps.storage, denom, |one| { - StdResult::Ok(one.unwrap_or(Uint128::zero()) + amount) + let mut new = one.unwrap_or((remote_denom, Uint128::zero())); + new.1 += amount; + StdResult::Ok(new) })?; } @@ -995,15 +1024,52 @@ fn get_non_native_rewards_transfer_msg( }))) } +fn get_pending_redeem_msg( + deps: Deps, + config: &Config, + env: &Env, + funds: Vec, +) -> ContractResult>> { + if LSM_SHARES_TO_REDEEM + .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .count() + < config.lsm_redeem_threshold as usize + { + return Ok(None); + } + let shares_to_redeeem = LSM_SHARES_TO_REDEEM + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::>>()?; + let items = shares_to_redeeem + .iter() + .map(|(local_denom, (denom, amount))| RedeemShareItem { + amount: *amount, + local_denom: local_denom.to_string(), + remote_denom: denom.to_string(), + }) + .collect(); + Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.puppeteer_contract.to_string(), + msg: to_json_binary( + &lido_staking_base::msg::puppeteer::ExecuteMsg::RedeemShares { + items, + timeout: Some(config.puppeteer_timeout), + reply_to: env.contract.address.to_string(), + }, + )?, + funds, + }))) +} + fn get_pending_lsm_share_msg( deps: DepsMut, config: &Config, env: &Env, funds: Vec, ) -> ContractResult>> { - let lsm_share: Option<(String, Uint128)> = PENDING_LSM_SHARES.first(deps.storage)?; + let lsm_share: Option<(String, (String, Uint128))> = PENDING_LSM_SHARES.first(deps.storage)?; match lsm_share { - Some((denom, amount)) => Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { + Some((denom, (_remote_denom, amount))) => Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.puppeteer_contract.to_string(), msg: to_json_binary( &lido_staking_base::msg::puppeteer::ExecuteMsg::IBCTransfer { @@ -1027,7 +1093,7 @@ mod check_denom { #[derive(PartialEq)] pub enum DenomType { Base, - LsmShare, + LsmShare(String), } // XXX: cosmos_sdk_proto defines these structures for me, @@ -1075,7 +1141,6 @@ mod check_denom { if denom == config.base_denom { return Ok(DenomType::Base); } - let trace = query_denom_trace(deps, denom)?.denom_trace; let (port, channel) = trace .path @@ -1101,7 +1166,6 @@ mod check_denom { if validator_info.is_none() { return Err(ContractError::InvalidDenom {}); } - - Ok(DenomType::LsmShare) + Ok(DenomType::LsmShare(trace.base_denom)) } } diff --git a/contracts/hook-tester/src/contract.rs b/contracts/hook-tester/src/contract.rs index ca8f217c..47b9877e 100644 --- a/contracts/hook-tester/src/contract.rs +++ b/contracts/hook-tester/src/contract.rs @@ -3,7 +3,10 @@ use cosmwasm_std::{ Response, StdResult, Uint128, WasmMsg, }; use lido_helpers::answer::response; -use lido_puppeteer_base::msg::{ResponseHookErrorMsg, ResponseHookMsg, ResponseHookSuccessMsg}; +use lido_puppeteer_base::{ + msg::{ResponseHookErrorMsg, ResponseHookMsg, ResponseHookSuccessMsg}, + state::RedeemShareItem, +}; use lido_staking_base::{ msg::hook_tester::{ExecuteMsg, InstantiateMsg, QueryMsg}, state::hook_tester::{Config, ANSWERS, CONFIG, ERRORS}, @@ -252,10 +255,12 @@ fn execute_redeem_share( let msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.puppeteer_addr, msg: to_json_binary( - &lido_staking_base::msg::puppeteer::ExecuteMsg::RedeemShare { - validator, - amount, - denom, + &lido_staking_base::msg::puppeteer::ExecuteMsg::RedeemShares { + items: vec![RedeemShareItem { + remote_denom: denom, + amount, + local_denom: "some".to_string(), + }], timeout, reply_to: env.contract.address.to_string(), }, diff --git a/contracts/puppeteer/src/contract.rs b/contracts/puppeteer/src/contract.rs index da088730..920b4926 100644 --- a/contracts/puppeteer/src/contract.rs +++ b/contracts/puppeteer/src/contract.rs @@ -35,7 +35,8 @@ use lido_puppeteer_base::{ }, proto::MsgIBCTransfer, state::{ - PuppeteerBase, ReplyMsg, TxState, TxStateStatus, UnbondingDelegation, ICA_ID, LOCAL_DENOM, + PuppeteerBase, RedeemShareItem, ReplyMsg, TxState, TxStateStatus, UnbondingDelegation, + ICA_ID, LOCAL_DENOM, }, }; use lido_staking_base::{ @@ -194,13 +195,11 @@ pub fn execute( timeout, reply_to, } => execute_tokenize_share(deps, info, validator, amount, timeout, reply_to), - ExecuteMsg::RedeemShare { - validator, - amount, - denom, + ExecuteMsg::RedeemShares { + items, timeout, reply_to, - } => execute_redeem_share(deps, info, validator, amount, denom, timeout, reply_to), + } => execute_redeem_shares(deps, info, items, timeout, reply_to), ExecuteMsg::ClaimRewardsAndOptionalyTransfer { validators, transfer, @@ -712,20 +711,16 @@ fn execute_tokenize_share( Ok(Response::default().add_submessages(vec![submsg])) } -fn execute_redeem_share( +fn execute_redeem_shares( mut deps: DepsMut, info: MessageInfo, - validator: String, - amount: Uint128, - denom: String, + items: Vec, timeout: Option, reply_to: String, ) -> ContractResult> { let attrs = vec![ attr("action", "redeem_share"), - attr("validator", validator.clone()), - attr("amount", amount.to_string()), - attr("denom", denom.clone()), + attr("items", format!("{:?}", items)), ]; let puppeteer_base = Puppeteer::default(); deps.api.addr_validate(&reply_to)?; @@ -733,25 +728,24 @@ fn execute_redeem_share( let config: Config = puppeteer_base.config.load(deps.storage)?; validate_sender(&config, &info.sender)?; let delegator = puppeteer_base.ica.get_address(deps.storage)?; - let redeem_msg = MsgRedeemTokensforShares { - delegator_address: delegator, - amount: Some(ProtoCoin { - denom: denom.to_string(), - amount: amount.to_string(), - }), - }; + let any_msgs = items + .iter() + .map(|one| MsgRedeemTokensforShares { + delegator_address: delegator.to_string(), + amount: Some(ProtoCoin { + denom: one.remote_denom.to_string(), + amount: one.amount.to_string(), + }), + }) + .map(|msg| prepare_any_msg(msg, "/cosmos.staking.v1beta1.MsgRedeemTokensForShares")) + .collect::>>()?; let submsg = compose_submsg( deps.branch(), config, - vec![prepare_any_msg( - redeem_msg, - "/cosmos.staking.v1beta1.MsgRedeemTokensForShares", - )?], - Transaction::RedeemShare { + any_msgs, + Transaction::RedeemShares { interchain_account_id: ICA_ID.to_string(), - validator, - denom, - amount: amount.into(), + items, }, timeout, reply_to, diff --git a/integration_tests/src/generated/contractLib/lidoCore.ts b/integration_tests/src/generated/contractLib/lidoCore.ts index b3d5ad65..48823224 100644 --- a/integration_tests/src/generated/contractLib/lidoCore.ts +++ b/integration_tests/src/generated/contractLib/lidoCore.ts @@ -26,6 +26,21 @@ export type ContractState = "idle" | "claiming" | "unbonding" | "staking" | "tra * The greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18) */ export type Decimal = string; +/** + * A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. + * + * # Examples + * + * Use `from` to create instances of this and `u128` to get the value out: + * + * ``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123); + * + * let b = Uint128::from(42u64); assert_eq!(b.u128(), 42); + * + * let c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ``` + */ +export type Uint128 = string; +export type ArrayOfTupleOf_StringAnd_TupleOf_StringAnd_Uint128 = [string, [string, Uint128]][]; export type ResponseHookMsg = | { success: ResponseHookSuccessMsg; @@ -61,20 +76,6 @@ export type ResponseAnswer = | { unknown_response: {}; }; -/** - * A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. - * - * # Examples - * - * Use `from` to create instances of this and `u128` to get the value out: - * - * ``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123); - * - * let b = Uint128::from(42u64); assert_eq!(b.u128(), 42); - * - * let c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ``` - */ -export type Uint128 = string; /** * Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline. * @@ -121,11 +122,9 @@ export type Transaction = }; } | { - redeem_share: { - amount: number; - denom: string; + redeem_shares: { interchain_account_id: string; - validator: string; + items: RedeemShareItem[]; }; } | { @@ -151,7 +150,7 @@ export type Transaction = }; export type ArrayOfNonNativeRewardsItem = NonNativeRewardsItem[]; export type String = string; -export type ArrayOfTupleOf_StringAnd_Uint128 = [string, Uint128][]; +export type ArrayOfTupleOf_StringAnd_TupleOf_StringAnd_Uint1281 = [string, [string, Uint128]][]; /** * A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0 * @@ -221,10 +220,11 @@ export interface LidoCoreSchema { | Config | ContractState | Decimal + | ArrayOfTupleOf_StringAnd_TupleOf_StringAnd_Uint128 | ResponseHookMsg | ArrayOfNonNativeRewardsItem | String - | ArrayOfTupleOf_StringAnd_Uint128 + | ArrayOfTupleOf_StringAnd_TupleOf_StringAnd_Uint1281 | UnbondBatch; query: UnbondBatchArgs; execute: @@ -304,6 +304,11 @@ export interface RequestPacketTimeoutHeight { revision_number?: number | null; [k: string]: unknown; } +export interface RedeemShareItem { + amount: Uint128; + local_denom: string; + remote_denom: string; +} export interface TransferReadyBatchMsg { amount: Uint128; batch_id: number; @@ -424,9 +429,12 @@ export class Client { queryNonNativeRewardsReceivers = async(): Promise => { return this.client.queryContractSmart(this.contractAddress, { non_native_rewards_receivers: {} }); } - queryPendingLSMShares = async(): Promise => { + queryPendingLSMShares = async(): Promise => { return this.client.queryContractSmart(this.contractAddress, { pending_l_s_m_shares: {} }); } + queryLSMSharesToRedeem = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { l_s_m_shares_to_redeem: {} }); + } 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); diff --git a/integration_tests/src/generated/contractLib/lidoHookTester.ts b/integration_tests/src/generated/contractLib/lidoHookTester.ts index 3500cd9d..f6b2e0cf 100644 --- a/integration_tests/src/generated/contractLib/lidoHookTester.ts +++ b/integration_tests/src/generated/contractLib/lidoHookTester.ts @@ -89,11 +89,9 @@ export type Transaction = }; } | { - redeem_share: { - amount: number; - denom: string; + redeem_shares: { interchain_account_id: string; - validator: string; + items: RedeemShareItem[]; }; } | { @@ -188,6 +186,11 @@ export interface RequestPacketTimeoutHeight { revision_number?: number | null; [k: string]: unknown; } +export interface RedeemShareItem { + amount: Uint128; + local_denom: string; + remote_denom: string; +} export interface TransferReadyBatchMsg { amount: Uint128; batch_id: number; diff --git a/integration_tests/src/generated/contractLib/lidoPuppeteer.ts b/integration_tests/src/generated/contractLib/lidoPuppeteer.ts index 9abb320e..bafd845f 100644 --- a/integration_tests/src/generated/contractLib/lidoPuppeteer.ts +++ b/integration_tests/src/generated/contractLib/lidoPuppeteer.ts @@ -62,11 +62,9 @@ export type Transaction = }; } | { - redeem_share: { - amount: number; - denom: string; + redeem_shares: { interchain_account_id: string; - validator: string; + items: RedeemShareItem[]; }; } | { @@ -134,7 +132,7 @@ export interface LidoPuppeteerSchema { | UndelegateArgs | RedelegateArgs | TokenizeShareArgs - | RedeemShareArgs + | RedeemSharesArgs | IBCTransferArgs | TransferArgs | ClaimRewardsAndOptionalyTransferArgs; @@ -145,6 +143,11 @@ export interface ConfigResponse { owner: string; update_period: number; } +export interface RedeemShareItem { + amount: Uint128; + local_denom: string; + remote_denom: string; +} export interface TransferReadyBatchMsg { amount: Uint128; batch_id: number; @@ -197,12 +200,10 @@ export interface TokenizeShareArgs { timeout?: number | null; validator: string; } -export interface RedeemShareArgs { - amount: Uint128; - denom: string; +export interface RedeemSharesArgs { + items: RedeemShareItem[]; reply_to: string; timeout?: number | null; - validator: string; } export interface IBCTransferArgs { reply_to: string; @@ -303,9 +304,9 @@ export class Client { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { tokenize_share: args }, fee || "auto", memo, funds); } - redeemShare = async(sender:string, args: RedeemShareArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + redeemShares = async(sender:string, args: RedeemSharesArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } - return this.client.execute(sender, this.contractAddress, { redeem_share: args }, fee || "auto", memo, funds); + return this.client.execute(sender, this.contractAddress, { redeem_shares: args }, fee || "auto", memo, funds); } iBCTransfer = async(sender:string, args: IBCTransferArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } diff --git a/integration_tests/src/testcases/core.fsm.test.ts b/integration_tests/src/testcases/core.fsm.test.ts index 08a1b15b..773a6c41 100644 --- a/integration_tests/src/testcases/core.fsm.test.ts +++ b/integration_tests/src/testcases/core.fsm.test.ts @@ -1192,231 +1192,366 @@ describe('Core', () => { describe('fourth cycle (LSM-shares)', () => { let lsmDenoms: string[] = []; let oldBalanceDenoms: string[] = []; - describe('create LSM shares and send them to neutron', () => { - it('get balances', async () => { - const oldBalances = - await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - context.neutronUserAddress, - ); - oldBalanceDenoms = oldBalances.data.balances.map((b) => b.denom); + let exchangeRate = ''; + describe('prepare', () => { + it('get exchange rate', async () => { + exchangeRate = await context.coreContractClient.queryExchangeRate(); }); - it('delegate', async () => { - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking delegate ${context.validatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking delegate ${context.secondValidatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; + describe('create LSM shares and send them to neutron', () => { + it('get balances', async () => { + const oldBalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + oldBalanceDenoms = oldBalances.data.balances.map((b) => b.denom); + }); + it('delegate', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.validatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.secondValidatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + }); + it('tokenize shares', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.validatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find( + (a) => a.denom == `${context.validatorAddress}/1`, + ), + ).toEqual({ + denom: `${context.validatorAddress}/1`, + amount: '60000', + }); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.secondValidatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find( + (a) => a.denom == `${context.secondValidatorAddress}/2`, + ), + ).toEqual({ + denom: `${context.secondValidatorAddress}/2`, + amount: '60000', + }); + } + }); + it('transfer shares to neutron', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.validatorAddress}/1 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + await sleep(5_000); + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.secondValidatorAddress}/2 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + let tx: IndexedTx | null = null; + await waitFor(async () => { + tx = await context.gaiaClient.getTx(out.txhash); + return tx !== null; + }); + expect(tx.height).toBeGreaterThan(0); + expect(tx.code).toBe(0); + } + }); + it('wait for balances to come', async () => { await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } + const newbalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + const newDenoms = newbalances.data.balances.map((b) => b.denom); + const diff = newDenoms.filter( + (d) => !oldBalanceDenoms.includes(d), + ); + lsmDenoms = diff; + return diff.length === 2; + }, 60_000); + }); }); - it('tokenize shares', async () => { + + it('bond LSM shares', async () => { { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking tokenize-share ${context.validatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - const balances = await context.gaiaQueryClient.bank.allBalances( - context.gaiaUserAddress, + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '60000', + denom: lsmDenoms[0], + }, + ], ); - expect( - balances.find((a) => a.denom == `${context.validatorAddress}/1`), - ).toEqual({ - denom: `${context.validatorAddress}/1`, - amount: '60000', - }); + expect(res.transactionHash).toHaveLength(64); } { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx staking tokenize-share ${context.secondValidatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - const balances = await context.gaiaQueryClient.bank.allBalances( - context.gaiaUserAddress, + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '60000', + denom: lsmDenoms[1], + }, + ], ); - expect( - balances.find( - (a) => a.denom == `${context.secondValidatorAddress}/2`, - ), - ).toEqual({ - denom: `${context.secondValidatorAddress}/2`, - amount: '60000', - }); + expect(res.transactionHash).toHaveLength(64); } }); - it('transfer shares to neutron', async () => { - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.validatorAddress}/1 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, - ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } - { - const res = await context.park.executeInNetwork( - 'gaia', - `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.secondValidatorAddress}/2 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + it('verify pending lsm shares', async () => { + const pending = + await context.coreContractClient.queryPendingLSMShares(); + expect(pending).toHaveLength(2); + }); + }); + describe('transfering', () => { + 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('one lsm share is gone from the contract balance', async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.coreContractClient.contractAddress, ); - expect(res.exitCode).toBe(0); - const out = JSON.parse(res.out); - expect(out.code).toBe(0); - expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); - } + expect( + balances.data.balances.find((one) => one.denom === lsmDenoms[0]), + ).toBeFalsy(); }); - it('wait for balances to come', async () => { + it('await for pending length decrease', async () => { + let pending: any; await waitFor(async () => { - const newbalances = - await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - context.neutronUserAddress, - ); - const newDenoms = newbalances.data.balances.map((b) => b.denom); - const diff = newDenoms.filter((d) => !oldBalanceDenoms.includes(d)); - lsmDenoms = diff; - return diff.length === 2; + try { + const res = + await context.coreContractClient.queryPendingLSMShares(); + pending = res; + } catch (e) { + // + } + return !!pending && pending.length === 1; }, 60_000); }); - }); - it('bond LSM shares', async () => { - { - const { coreContractClient, neutronUserAddress } = context; - const res = await coreContractClient.bond( + it('tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( neutronUserAddress, - {}, - 1.6, + 1.5, undefined, - [ - { - amount: '60000', - denom: lsmDenoms[0], - }, - ], + [], ); expect(res.transactionHash).toHaveLength(64); - } - { - const { coreContractClient, neutronUserAddress } = context; - const res = await coreContractClient.bond( + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + it('second lsm share is gone from the contract balance', async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.coreContractClient.contractAddress, + ); + expect( + balances.data.balances.find((one) => one.denom === lsmDenoms[1]), + ).toBeFalsy(); + }); + it('await for pending length decrease', async () => { + let pending: any; + await waitFor(async () => { + try { + const res = + await context.coreContractClient.queryPendingLSMShares(); + pending = res; + } catch (e) { + // + } + return !!pending && pending.length === 0; + }, 60_000); + expect(pending).toEqual([]); + }); + }); + describe('redeem', () => { + let delegationsSum = 0; + it('query delegations', async () => { + const res: any = await context.puppeteerContractClient.queryExtention( + { + msg: { + delegations: {}, + }, + }, + ); + for (const d of res[0].delegations) { + delegationsSum += parseInt(d.amount.amount); + } + }); + it('verify pending lsm shares to unbond', async () => { + const pending = + await context.coreContractClient.queryLSMSharesToRedeem(); + expect(pending).toHaveLength(2); + }); + it('tick', async () => { + const { neutronUserAddress } = context; + const res = await context.coreContractClient.tick( neutronUserAddress, - {}, - 1.6, + 1.5, undefined, - [ - { - amount: '60000', - denom: lsmDenoms[1], - }, - ], + [], ); expect(res.transactionHash).toHaveLength(64); - } - }); - it('verify pending lsm shares', async () => { - const pending = - await context.coreContractClient.queryPendingLSMShares(); - expect(pending).toEqual([ - [lsmDenoms[0], '60000'], - [lsmDenoms[1], '60000'], - ]); - }); - 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('lsm shares are not on contract balance', async () => { - const balances = - await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( - context.coreContractClient.contractAddress, + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + it('imeediately tick again fails', async () => { + const { neutronUserAddress } = context; + await expect( + context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ), + ).rejects.toThrowError( + /Transaction txState is not equal to expected: Idle/, ); - expect( - balances.data.balances.find((one) => one.denom === lsmDenoms[0]), - ).toBeFalsy(); - }); - it('await for pending length decrease', async () => { - let pending: any; - await waitFor(async () => { - try { - const res = - await context.coreContractClient.queryPendingLSMShares(); - pending = res; - } catch (e) { - // + }); + it('await for pending length decrease', async () => { + await waitFor(async () => { + const pending = + await context.coreContractClient.queryLSMSharesToRedeem(); + return pending.length === 0; + }, 30_000); + }); + it('wait for delegations to come', async () => { + const [, currentHeight] = + await context.puppeteerContractClient.queryExtention({ + msg: { + delegations: {}, + }, + }); + await waitFor(async () => { + const [, nowHeight] = + await context.puppeteerContractClient.queryExtention({ + msg: { + delegations: {}, + }, + }); + return nowHeight !== currentHeight; + }); + }); + it('query delegations', async () => { + const res: any = await context.puppeteerContractClient.queryExtention( + { + msg: { + delegations: {}, + }, + }, + ); + let newDelegationsSum = 0; + for (const d of res[0].delegations) { + newDelegationsSum += parseInt(d.amount.amount); } - return !!pending && pending.length === 1; - }, 60_000); - expect(pending).toEqual([[lsmDenoms[1], '60000']]); + expect(newDelegationsSum - delegationsSum).toEqual(120_000); + }); + it('verify exchange rate', async () => { + const newExchangeRate = + await context.coreContractClient.queryExchangeRate(); + expect(parseFloat(newExchangeRate)).toBeGreaterThan( + parseFloat(exchangeRate), + ); + }); }); }); }); diff --git a/packages/base/src/msg/core.rs b/packages/base/src/msg/core.rs index fad66c09..32ae78d8 100644 --- a/packages/base/src/msg/core.rs +++ b/packages/base/src/msg/core.rs @@ -42,8 +42,10 @@ pub enum QueryMsg { LastPuppeteerResponse {}, #[returns(Vec)] NonNativeRewardsReceivers {}, - #[returns(Vec<(String,Uint128)>)] + #[returns(Vec<(String,(String, Uint128))>)] PendingLSMShares {}, + #[returns(Vec<(String,(String, Uint128))>)] + LSMSharesToRedeem {}, } #[cw_ownable_execute] diff --git a/packages/base/src/msg/puppeteer.rs b/packages/base/src/msg/puppeteer.rs index 97644f30..cbc6a09b 100644 --- a/packages/base/src/msg/puppeteer.rs +++ b/packages/base/src/msg/puppeteer.rs @@ -7,7 +7,10 @@ use cosmos_sdk_proto::cosmos::{ base::v1beta1::Coin as CosmosCoin, staking::v1beta1::{Delegation, Validator as CosmosValidator}, }; -use lido_puppeteer_base::msg::{ExecuteMsg as BaseExecuteMsg, TransferReadyBatchMsg}; +use lido_puppeteer_base::{ + msg::{ExecuteMsg as BaseExecuteMsg, TransferReadyBatchMsg}, + state::RedeemShareItem, +}; use neutron_sdk::{ bindings::types::StorageValue, interchain_queries::v045::types::{Balances, Delegations}, @@ -71,10 +74,8 @@ pub enum ExecuteMsg { timeout: Option, reply_to: String, }, - RedeemShare { - validator: String, - amount: Uint128, - denom: String, + RedeemShares { + items: Vec, timeout: Option, reply_to: String, }, diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index 60757c51..466340e4 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -65,8 +65,8 @@ pub struct UnbondBatch { pub const UNBOND_BATCHES: Map = Map::new("batches"); pub const UNBOND_BATCH_ID: Item = Item::new("batches_ids"); pub const TOTAL_LSM_SHARES: Item = Item::new("total_lsm_shares"); -pub const PENDING_LSM_SHARES: Map = Map::new("pending_lsm_shares"); -pub const LSM_SHARES_TO_REDEEM: Map = Map::new("lsm_shares_to_redeem"); +pub const PENDING_LSM_SHARES: Map = Map::new("pending_lsm_shares"); +pub const LSM_SHARES_TO_REDEEM: Map = Map::new("lsm_shares_to_redeem"); #[cw_serde] pub enum ContractState { diff --git a/packages/puppeteer-base/src/msg.rs b/packages/puppeteer-base/src/msg.rs index b802304f..7487cf15 100644 --- a/packages/puppeteer-base/src/msg.rs +++ b/packages/puppeteer-base/src/msg.rs @@ -1,7 +1,10 @@ -use crate::proto::{ - MsgBeginRedelegateResponse, MsgDelegateResponse, MsgExecResponse, MsgIBCTransfer, - MsgRedeemTokensforSharesResponse, MsgSendResponse, MsgTokenizeSharesResponse, - MsgUndelegateResponse, +use crate::{ + proto::{ + MsgBeginRedelegateResponse, MsgDelegateResponse, MsgExecResponse, MsgIBCTransfer, + MsgRedeemTokensforSharesResponse, MsgSendResponse, MsgTokenizeSharesResponse, + MsgUndelegateResponse, + }, + state::RedeemShareItem, }; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Empty, Uint128}; @@ -126,11 +129,9 @@ pub enum Transaction { denom: String, amount: u128, }, - RedeemShare { + RedeemShares { interchain_account_id: String, - validator: String, - denom: String, - amount: u128, + items: Vec, }, ClaimRewardsAndOptionalyTransfer { interchain_account_id: String, diff --git a/packages/puppeteer-base/src/state.rs b/packages/puppeteer-base/src/state.rs index 43fbf857..5ad88c5e 100644 --- a/packages/puppeteer-base/src/state.rs +++ b/packages/puppeteer-base/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::Coin; +use cosmwasm_std::{Coin, Uint128}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, UniqueIndex}; use lido_helpers::ica::Ica; use neutron_sdk::{bindings::msg::IbcFee, interchain_queries::v045::types::UnbondingEntry}; @@ -123,6 +123,13 @@ pub struct UnbondingDelegation { pub last_updated_height: u64, } +#[cw_serde] +pub struct RedeemShareItem { + pub amount: Uint128, + pub remote_denom: String, + pub local_denom: String, +} + pub struct UnbondingDelegationIndexes<'a> { pub query_id: UniqueIndex<'a, u64, UnbondingDelegation, String>, } From 50f0a476ff8beb70bacb62f6bd72b58af05c0cad Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Thu, 29 Feb 2024 15:33:48 +0100 Subject: [PATCH 5/7] fix: test --- integration_tests/src/testcases/auto-withdrawer.test.ts | 1 + integration_tests/src/testcases/core.fsm.test.ts | 2 +- integration_tests/src/testcases/core.test.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/integration_tests/src/testcases/auto-withdrawer.test.ts b/integration_tests/src/testcases/auto-withdrawer.test.ts index baa0b1ce..e85d3adc 100644 --- a/integration_tests/src/testcases/auto-withdrawer.test.ts +++ b/integration_tests/src/testcases/auto-withdrawer.test.ts @@ -342,6 +342,7 @@ describe('Auto withdrawer', () => { unbonding_safe_period: 10, unbonding_period: 60, channel: 'channel-0', + lsm_redeem_threshold: 2, }, }); expect(res.transactionHash).toHaveLength(64); diff --git a/integration_tests/src/testcases/core.fsm.test.ts b/integration_tests/src/testcases/core.fsm.test.ts index 773a6c41..38a3b395 100644 --- a/integration_tests/src/testcases/core.fsm.test.ts +++ b/integration_tests/src/testcases/core.fsm.test.ts @@ -1317,7 +1317,7 @@ describe('Core', () => { expect(tx.height).toBeGreaterThan(0); expect(tx.code).toBe(0); } - await sleep(5_000); + await sleep(10_000); { const res = await context.park.executeInNetwork( 'gaia', diff --git a/integration_tests/src/testcases/core.test.ts b/integration_tests/src/testcases/core.test.ts index 36bf3169..dd31697d 100644 --- a/integration_tests/src/testcases/core.test.ts +++ b/integration_tests/src/testcases/core.test.ts @@ -372,6 +372,7 @@ describe('Core', () => { unbonding_safe_period: 10, unbonding_period: 60, channel: 'channel-0', + lsm_redeem_threshold: 2, }, }); expect(res.transactionHash).toHaveLength(64); From 39e11a03b033454105d5b285134f12da2bc6ca89 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Mon, 11 Mar 2024 15:46:26 +0400 Subject: [PATCH 6/7] chore: bump ver --- integration_tests/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/package.json b/integration_tests/package.json index 91f95a44..92a39e2e 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": { @@ -54,4 +54,4 @@ }, "description": "Lido on Cosmos integration test", "repository": "git@github.com:hadronlabs-org/lionco-contracts.git" -} +} \ No newline at end of file From 1f945b06c4c6ad0830e61c21d779b3d9a1a14775 Mon Sep 17 00:00:00 2001 From: Sergey Ratiashvili Date: Wed, 13 Mar 2024 17:07:47 +0100 Subject: [PATCH 7/7] feat: refactor --- integration_tests/src/helpers/waitForTx.ts | 23 ++ integration_tests/src/testcases/core.test.ts | 359 +++++++++++++++++-- 2 files changed, 359 insertions(+), 23 deletions(-) create mode 100644 integration_tests/src/helpers/waitForTx.ts diff --git a/integration_tests/src/helpers/waitForTx.ts b/integration_tests/src/helpers/waitForTx.ts new file mode 100644 index 00000000..1213e38d --- /dev/null +++ b/integration_tests/src/helpers/waitForTx.ts @@ -0,0 +1,23 @@ +import { SigningStargateClient, StargateClient } from '@cosmjs/stargate'; +import { waitFor } from './waitFor'; + +export const waitForTx = async ( + client: SigningStargateClient | StargateClient, + hash: string, + timeout: number = 10000, + interval: number = 600, +): Promise => + await waitFor( + async () => { + const tx = await client.getTx(hash); + if (tx === null) { + return false; + } + if (tx.code !== 0) { + throw new Error(`Transaction failed with code: ${tx.code}`); + } + return tx.code === 0 && tx.height > 0; + }, + timeout, + interval, + ); diff --git a/integration_tests/src/testcases/core.test.ts b/integration_tests/src/testcases/core.test.ts index 3f66c582..674ebcf7 100644 --- a/integration_tests/src/testcases/core.test.ts +++ b/integration_tests/src/testcases/core.test.ts @@ -39,6 +39,7 @@ import { } from '../generated/contractLib/lidoCore'; import { stringToPath } from '@cosmjs/crypto'; import { sleep } from '../helpers/sleep'; +import { waitForTx } from '../helpers/waitForTx'; const LidoFactoryClass = LidoFactory.Client; const LidoCoreClass = LidoCore.Client; @@ -558,13 +559,7 @@ describe('Core', () => { const out = JSON.parse(res.out); expect(out.code).toBe(0); expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); + await waitForTx(context.gaiaClient, out.txhash); }); it('tokenize share on gaia side', async () => { const res = await context.park.executeInNetwork( @@ -575,13 +570,7 @@ describe('Core', () => { const out = JSON.parse(res.out); expect(out.code).toBe(0); expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); + await waitForTx(context.gaiaClient, out.txhash); const balances = await context.gaiaQueryClient.bank.allBalances( context.gaiaUserAddress, ); @@ -601,13 +590,7 @@ describe('Core', () => { const out = JSON.parse(res.out); expect(out.code).toBe(0); expect(out.txhash).toHaveLength(64); - let tx: IndexedTx | null = null; - await waitFor(async () => { - tx = await context.gaiaClient.getTx(out.txhash); - return tx !== null; - }); - expect(tx.height).toBeGreaterThan(0); - expect(tx.code).toBe(0); + await waitForTx(context.gaiaClient, out.txhash); }); it('wait for neutron to receive tokenized share', async () => { const { neutronClient, neutronUserAddress } = context; @@ -1214,7 +1197,7 @@ describe('Core', () => { expect(state).toEqual('idle'); }); }); - describe('third cycle', () => { + describe('third cycle (non-native rewards)', () => { let remoteNonNativeDenoms: string[] = []; it('generate two new tokenfactory tokens and send them to the remote zone', async () => { const { neutronUserAddress } = context; @@ -1400,7 +1383,337 @@ describe('Core', () => { }); }); - describe('fourth cycle', () => { + describe('fourth cycle (LSM-shares)', () => { + let lsmDenoms: string[] = []; + let oldBalanceDenoms: string[] = []; + let exchangeRate = ''; + describe('prepare', () => { + it('get exchange rate', async () => { + exchangeRate = await context.coreContractClient.queryExchangeRate(); + }); + describe('create LSM shares and send them to neutron', () => { + it('get balances', async () => { + const oldBalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + oldBalanceDenoms = oldBalances.data.balances.map((b) => b.denom); + }); + it('delegate', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.validatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + await waitForTx(context.gaiaClient, out.txhash); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking delegate ${context.secondValidatorAddress} 100000stake --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + await waitForTx(context.gaiaClient, out.txhash); + } + }); + it('tokenize shares', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.validatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + await waitForTx(context.gaiaClient, out.txhash); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find( + (a) => a.denom == `${context.validatorAddress}/2`, + ), + ).toEqual({ + denom: `${context.validatorAddress}/2`, + amount: '60000', + }); + } + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx staking tokenize-share ${context.secondValidatorAddress} 60000stake ${context.gaiaUserAddress} --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + await waitForTx(context.gaiaClient, out.txhash); + const balances = await context.gaiaQueryClient.bank.allBalances( + context.gaiaUserAddress, + ); + expect( + balances.find( + (a) => a.denom == `${context.secondValidatorAddress}/3`, + ), + ).toEqual({ + denom: `${context.secondValidatorAddress}/3`, + amount: '60000', + }); + } + }); + it('transfer shares to neutron', async () => { + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.validatorAddress}/2 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + await waitForTx(context.gaiaClient, out.txhash); + } + await sleep(10_000); + { + const res = await context.park.executeInNetwork( + 'gaia', + `gaiad tx ibc-transfer transfer transfer channel-0 ${context.neutronUserAddress} 60000${context.secondValidatorAddress}/3 --from ${context.gaiaUserAddress} --yes --chain-id testgaia --home=/opt --keyring-backend=test --gas auto --gas-adjustment 2 --output json`, + ); + expect(res.exitCode).toBe(0); + const out = JSON.parse(res.out); + expect(out.code).toBe(0); + expect(out.txhash).toHaveLength(64); + await waitForTx(context.gaiaClient, out.txhash); + } + }); + it('wait for balances to come', async () => { + await waitFor(async () => { + const newbalances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.neutronUserAddress, + ); + const newDenoms = newbalances.data.balances.map((b) => b.denom); + const diff = newDenoms.filter( + (d) => !oldBalanceDenoms.includes(d), + ); + lsmDenoms = diff; + return diff.length === 2; + }, 30_000); + }); + }); + + it('bond LSM shares', async () => { + { + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '60000', + denom: lsmDenoms[0], + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + } + { + const { coreContractClient, neutronUserAddress } = context; + const res = await coreContractClient.bond( + neutronUserAddress, + {}, + 1.6, + undefined, + [ + { + amount: '60000', + denom: lsmDenoms[1], + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); + } + }); + it('verify pending lsm shares', async () => { + const pending = + await context.coreContractClient.queryPendingLSMShares(); + expect(pending).toHaveLength(2); + }); + }); + describe('transfering', () => { + 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('one lsm share is gone from the contract balance', async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.coreContractClient.contractAddress, + ); + expect( + balances.data.balances.find((one) => one.denom === lsmDenoms[0]), + ).toBeFalsy(); + }); + it('await for pending length decrease', async () => { + let pending: any; + await waitFor(async () => { + try { + const res = + await context.coreContractClient.queryPendingLSMShares(); + pending = res; + } catch (e) { + // + } + return !!pending && pending.length === 1; + }, 60_000); + }); + 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('second lsm share is gone from the contract balance', async () => { + const balances = + await context.neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + context.coreContractClient.contractAddress, + ); + expect( + balances.data.balances.find((one) => one.denom === lsmDenoms[1]), + ).toBeFalsy(); + }); + it('await for pending length decrease', async () => { + let pending: any; + await waitFor(async () => { + try { + const res = + await context.coreContractClient.queryPendingLSMShares(); + pending = res; + } catch (e) { + // + } + return !!pending && pending.length === 0; + }, 60_000); + expect(pending).toEqual([]); + }); + }); + describe('redeem', () => { + let delegationsSum = 0; + it('query delegations', async () => { + const res: any = await context.puppeteerContractClient.queryExtention( + { + msg: { + delegations: {}, + }, + }, + ); + for (const d of res[0].delegations) { + delegationsSum += parseInt(d.amount.amount); + } + }); + it('verify pending lsm shares to unbond', async () => { + const pending = + await context.coreContractClient.queryLSMSharesToRedeem(); + expect(pending).toHaveLength(2); + }); + 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('imeediately tick again fails', async () => { + const { neutronUserAddress } = context; + await expect( + context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ), + ).rejects.toThrowError( + /Transaction txState is not equal to expected: Idle/, + ); + }); + it('await for pending length decrease', async () => { + await waitFor(async () => { + const pending = + await context.coreContractClient.queryLSMSharesToRedeem(); + return pending.length === 0; + }, 30_000); + }); + it('wait for delegations to come', async () => { + const [, currentHeight] = + await context.puppeteerContractClient.queryExtention({ + msg: { + delegations: {}, + }, + }); + await waitFor(async () => { + const [, nowHeight] = + await context.puppeteerContractClient.queryExtention({ + msg: { + delegations: {}, + }, + }); + return nowHeight !== currentHeight; + }); + }); + it('query delegations', async () => { + const res: any = await context.puppeteerContractClient.queryExtention( + { + msg: { + delegations: {}, + }, + }, + ); + let newDelegationsSum = 0; + for (const d of res[0].delegations) { + newDelegationsSum += parseInt(d.amount.amount); + } + expect(newDelegationsSum - delegationsSum).toEqual(120_000); + }); + it('verify exchange rate', async () => { + const newExchangeRate = + await context.coreContractClient.queryExchangeRate(); + expect(parseFloat(newExchangeRate)).toBeGreaterThan( + parseFloat(exchangeRate), + ); + }); + }); + }); + + describe('fifth cycle', () => { let previousResponse: ResponseHookSuccessMsg; it('validate NFT', async () => {