From dfeb5953334c04c2f5a2ec849eefe230200940c6 Mon Sep 17 00:00:00 2001 From: Murad Karammaev Date: Wed, 13 Mar 2024 21:22:25 +0200 Subject: [PATCH] feat: corner case: multiple unbonded batches --- .github/workflows/build.yml | 6 +- .github/workflows/tests.yml | 10 +- Makefile | 6 +- contracts/core/src/contract.rs | 238 ++-- contracts/core/src/error.rs | 3 + contracts/core/src/tests.rs | 4 + contracts/factory/src/contract.rs | 2 + contracts/factory/src/msg.rs | 1 + contracts/puppeteer/src/contract.rs | 4 +- integration_tests/package.json | 21 +- .../src/generated/contractLib/dropCore.ts | 20 +- .../src/generated/contractLib/dropFactory.ts | 3 + .../generated/contractLib/dropHookTester.ts | 9 +- .../generated/contractLib/dropPuppeteer.ts | 12 +- integration_tests/src/testSuite.ts | 4 +- .../src/testcases/auto-withdrawer.test.ts | 1 + .../src/testcases/core-slashing.test.ts | 1161 +++++++++++++++++ integration_tests/src/testcases/core.test.ts | 1 + integration_tests/src/testcases/pump.test.ts | 7 + integration_tests/test.json | 117 -- packages/base/src/msg/core.rs | 10 +- packages/base/src/msg/proposal_votes.rs | 7 +- packages/base/src/msg/provider_proposals.rs | 11 +- packages/base/src/msg/puppeteer.rs | 4 +- packages/base/src/msg/validatorset.rs | 9 +- packages/base/src/state/core.rs | 13 +- packages/puppeteer-base/src/msg.rs | 7 +- rust-toolchain.toml | 2 +- 28 files changed, 1426 insertions(+), 267 deletions(-) create mode 100644 integration_tests/src/testcases/core-slashing.test.ts delete mode 100644 integration_tests/test.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bc01b08..ae403a22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: inputs: toolchain: description: 'Default Rust Toolchain' - default: "1.73.0" + default: "1.75.0" required: true type: string target: @@ -31,7 +31,7 @@ on: type: string env: - TOOLCHAIN: ${{ inputs.toolchain || '1.73.0' }} + TOOLCHAIN: ${{ inputs.toolchain || '1.75.0' }} TARGET: ${{ inputs.target || 'wasm32-unknown-unknown' }} REF: ${{ github.event_name == 'push' && github.ref || inputs.branch || 'main' }} ID: ${{ inputs.id || 'scheduled' }} @@ -46,7 +46,7 @@ jobs: ref: ${{ env.REF }} fetch-depth: 0 - name: Save SHA - run: echo "sha=$(/usr/bin/git log -1 --format='%H')" >> $GITHUB_ENV + run: echo "sha=$(/usr/bin/git log -1 --format='%H')" >> $GITHUB_ENV - name: Check input type run: | if git show-ref --quiet --heads $REF; then diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab10102f..50292713 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,12 +15,14 @@ jobs: fetch-depth: 1 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.73.0 + toolchain: 1.75.0 components: clippy profile: minimal override: true + target: wasm32-unknown-unknown - run: cargo fetch --verbose - run: cargo clippy --all --all-targets -- -D warnings + - run: cargo clippy --lib --target wasm32-unknown-unknown -- -D warnings rustfmt: name: Actions - rustfmt @@ -31,7 +33,7 @@ jobs: fetch-depth: 1 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.73.0 + toolchain: 1.75.0 components: rustfmt profile: minimal override: true @@ -44,7 +46,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.73.0 + toolchain: 1.75.0 profile: minimal - run: cargo fetch --verbose - run: cargo build @@ -62,7 +64,7 @@ jobs: fetch-depth: 1 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.73.0 + toolchain: 1.75.0 profile: minimal override: true - name: Setup node diff --git a/Makefile b/Makefile index fe061f0d..f82f5abb 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,9 @@ test: @cargo test clippy: + @rustup target add wasm32-unknown-unknown @cargo clippy --all --all-targets -- -D warnings + @cargo clippy --lib --target wasm32-unknown-unknown -- -D warnings fmt: @cargo fmt -- --check @@ -19,7 +21,7 @@ compile: --mount type=volume,source="$(notdir $(CURDIR))_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/amd64 \ - cosmwasm/workspace-optimizer:0.15.0 + cosmwasm/workspace-optimizer:0.15.1 @sudo chown -R $(shell id -u):$(shell id -g) artifacts compile_arm64: @@ -27,7 +29,7 @@ compile_arm64: --mount type=volume,source="$(notdir $(CURDIR))_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/arm64 \ - cosmwasm/workspace-optimizer-arm64:0.15.0 + cosmwasm/workspace-optimizer-arm64:0.15.1 @cd artifacts && for file in *-aarch64.wasm; do cp -f "$$file" "$${file%-aarch64.wasm}.wasm"; done check_contracts: diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index d5522470..7f8fcfb8 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -1,13 +1,13 @@ use crate::error::{ContractError, ContractResult}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - attr, ensure, ensure_eq, ensure_ne, entry_point, to_json_binary, Addr, Attribute, BankQuery, - Binary, Coin, CosmosMsg, CustomQuery, Decimal, Deps, DepsMut, Env, MessageInfo, Order, - QueryRequest, Response, StdError, StdResult, Timestamp, Uint128, WasmMsg, + attr, ensure, ensure_eq, entry_point, to_json_binary, Addr, Attribute, BankQuery, Binary, Coin, + CosmosMsg, CustomQuery, Decimal, Deps, DepsMut, Env, MessageInfo, Order, QueryRequest, + Response, StdError, StdResult, Timestamp, Uint128, WasmMsg, }; use cw2::set_contract_version; use drop_helpers::answer::response; -use drop_puppeteer_base::msg::{IBCTransferReason, TransferReadyBatchMsg}; +use drop_puppeteer_base::msg::{IBCTransferReason, TransferReadyBatchesMsg}; use drop_puppeteer_base::state::RedeemShareItem; use drop_staking_base::state::core::{ unbond_batches_map, Config, ConfigOptional, ContractState, FeeItem, NonNativeRewardsItem, @@ -380,75 +380,112 @@ fn execute_tick_idle( return Err(ContractError::IdleMinIntervalIsNotReached {}); } } else { + let unbonding_batches = unbond_batches_map() + .idx + .status + .prefix(UnbondBatchStatus::Unbonding as u8) + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; ensure!( !is_unbonding_time_close( - deps.as_ref(), + &unbonding_batches, env.block.time.seconds(), config.unbonding_safe_period - )?, + ), ContractError::UnbondingTimeIsClose {} ); + let pump_address = config .pump_address .clone() .ok_or(ContractError::PumpAddressIsNotSet {})?; + let (ica_balance, _local_height, ica_balance_local_time) = get_ica_balance_by_denom( + deps.as_ref(), + &config.puppeteer_contract, + &config.remote_denom, + true, + )?; - // detect already unbonded batch, if there is one - let unbonding_batches = unbond_batches_map() - .idx - .status - .prefix(UnbondBatchStatus::Unbonding as u8) - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - if !unbonding_batches.is_empty() { - let (id, mut unbonding_batch) = unbonding_batches + let unbonded_batches = if !unbonding_batches.is_empty() { + unbonding_batches .into_iter() - // we only need the oldest Unbonding batch - .min_by_key(|(_, batch)| batch.expected_release) - // this `unwrap()` is safe to call since in this branch - // `unbonding_batches` is not empty - .unwrap(); - let (balance, _local_height, ica_balance_local_time) = get_ica_balance_by_denom( - deps.as_ref(), - &config.puppeteer_contract, - &config.remote_denom, - true, - )?; + .filter(|(_id, batch)| { + batch.expected_release <= env.block.time.seconds() + && batch.expected_release < ica_balance_local_time + }) + .collect::>() + } else { + vec![] + }; + + let transfer: Option = match unbonded_batches.len() { + 0 => None, // we have nothing to do + 1 => { + let (id, mut unbonding_batch) = unbonded_batches + .into_iter() + // `.next().unwrap()` is safe to call since in this match arm + // `unbonding_batches` always has only 1 item + .next() + .unwrap(); - if unbonding_batch.expected_release <= env.block.time.seconds() - && unbonding_batch.expected_release < ica_balance_local_time - { let (unbonded_amount, slashing_effect) = - if balance < unbonding_batch.expected_amount { + if ica_balance < unbonding_batch.expected_amount { ( - balance, - Decimal::from_ratio(balance, unbonding_batch.expected_amount), + ica_balance, + Decimal::from_ratio(ica_balance, unbonding_batch.expected_amount), ) } else { (unbonding_batch.expected_amount, Decimal::one()) }; unbonding_batch.unbonded_amount = Some(unbonded_amount); unbonding_batch.slashing_effect = Some(slashing_effect); - unbonding_batch.status = UnbondBatchStatus::Unbonded; + unbonding_batch.status = UnbondBatchStatus::Withdrawing; unbond_batches_map().save(deps.storage, id, &unbonding_batch)?; - } - } - - // process unbond if any already unbonded - // and claim rewards - let transfer: Option = match get_unbonded_batch(deps.as_ref())? { - Some((batch_id, mut batch)) => { - batch.status = UnbondBatchStatus::Withdrawing; - unbond_batches_map().save(deps.storage, batch_id, &batch)?; - Some(TransferReadyBatchMsg { - batch_id, - amount: batch - .unbonded_amount - .ok_or(ContractError::UnbondedAmountIsNotSet {})?, + Some(TransferReadyBatchesMsg { + batch_ids: vec![id], + emergency: false, + amount: unbonded_amount, recipient: pump_address, }) } - None => None, + _ => { + let total_expected_amount: Uint128 = unbonded_batches + .iter() + .map(|(_id, batch)| batch.expected_amount) + .sum(); + let (emergency, recipient, amount) = if ica_balance < total_expected_amount { + ( + true, + config + .emergency_address + .clone() + .ok_or(ContractError::EmergencyAddressIsNotSet {})?, + ica_balance, + ) + } else { + (false, pump_address, total_expected_amount) + }; + let mut batch_ids = vec![]; + for (id, mut batch) in unbonded_batches { + batch_ids.push(id); + if emergency { + batch.unbonded_amount = None; + batch.slashing_effect = None; + batch.status = UnbondBatchStatus::WithdrawingEmergency; + } else { + batch.unbonded_amount = Some(batch.expected_amount); + batch.slashing_effect = Some(Decimal::one()); + batch.status = UnbondBatchStatus::Withdrawing; + } + unbond_batches_map().save(deps.storage, id, &batch)?; + } + Some(TransferReadyBatchesMsg { + batch_ids, + emergency, + amount, + recipient, + }) + } }; let validators: Vec = deps.querier.query_wasm_smart( @@ -485,8 +522,8 @@ fn execute_tick_idle( FSM.go_to(deps.storage, ContractState::Transfering)?; PENDING_TRANSFER.save(deps.storage, &pending_amount)?; messages.push(transfer_msg); - } else { - let stake_msg = get_stake_msg(deps.branch(), &env, config, info.funds)?; + } else if let Some(stake_msg) = get_stake_msg(deps.branch(), &env, config, info.funds)? + { messages.push(stake_msg); FSM.go_to(deps.storage, ContractState::Staking)?; } @@ -529,12 +566,18 @@ fn execute_tick_claiming( .. } => { if let Some(transfer) = transfer { - let mut batch = - unbond_batches_map().load(deps.storage, transfer.batch_id)?; - batch.status = UnbondBatchStatus::Withdrawn; - attrs.push(attr("batch_id", transfer.batch_id.to_string())); - attrs.push(attr("unbond_batch_status", "withdrawn")); - unbond_batches_map().save(deps.storage, transfer.batch_id, &batch)?; + for id in transfer.batch_ids { + let mut batch = unbond_batches_map().load(deps.storage, id)?; + attrs.push(attr("batch_id", id.to_string())); + if transfer.emergency { + batch.status = UnbondBatchStatus::WithdrawnEmergency; + attrs.push(attr("unbond_batch_status", "withdrawn_emergency")); + } else { + batch.status = UnbondBatchStatus::Withdrawn; + attrs.push(attr("unbond_batch_status", "withdrawn")); + } + unbond_batches_map().save(deps.storage, id, &batch)?; + } } } _ => return Err(ContractError::InvalidTransaction {}), @@ -550,10 +593,11 @@ fn execute_tick_claiming( FSM.go_to(deps.storage, ContractState::Transfering)?; PENDING_TRANSFER.save(deps.storage, &pending_amount)?; messages.push(transfer_msg); - } else { - let stake_msg = get_stake_msg(deps.branch(), &env, config, info.funds)?; + } else if let Some(stake_msg) = get_stake_msg(deps.branch(), &env, config, info.funds)? { messages.push(stake_msg); FSM.go_to(deps.storage, ContractState::Staking)?; + } else { + FSM.go_to(deps.storage, ContractState::Idle)?; } attrs.push(attr("state", "unbonding")); Ok(response("execute-tick_claiming", CONTRACT_NAME, attrs).add_messages(messages)) @@ -567,15 +611,24 @@ fn execute_tick_transfering( ) -> ContractResult> { let _response_msg = get_received_puppeteer_response(deps.as_ref())?; LAST_PUPPETEER_RESPONSE.remove(deps.storage); - let stake_msg = get_stake_msg(deps.branch(), &env, config, info.funds)?; - FSM.go_to(deps.storage, ContractState::Staking)?; - Ok(response( + let mut response = response( "execute-tick_transfering", CONTRACT_NAME, - vec![attr("action", "tick_transfering"), attr("state", "staking")], - ) - .add_message(stake_msg)) + vec![attr("action", "tick_transfering")], + ); + + if let Some(stake_msg) = get_stake_msg(deps.branch(), &env, config, info.funds)? { + response = response + .add_message(stake_msg) + .add_attribute("state", "staking"); + FSM.go_to(deps.storage, ContractState::Staking)?; + } else { + response = response.add_attribute("state", "idle"); + FSM.go_to(deps.storage, ContractState::Idle)?; + } + + Ok(response) } fn execute_tick_staking( @@ -839,6 +892,14 @@ fn execute_update_config( attrs.push(attr("fee_address", &fee_address)); config.fee_address = Some(fee_address); } + if let Some(emergency_address) = new_config.emergency_address { + attrs.push(attr("emergency_address", &emergency_address)); + config.emergency_address = Some(emergency_address); + } + if let Some(min_stake_amount) = new_config.min_stake_amount { + attrs.push(attr("min_stake_amount", min_stake_amount)); + config.min_stake_amount = min_stake_amount; + } CONFIG.save(deps.storage, &config)?; @@ -916,7 +977,7 @@ fn execute_unbond( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.token_contract, msg: to_json_binary(&TokenExecuteMsg::Burn {})?, - funds: vec![cosmwasm_std::Coin { + funds: vec![Coin { denom: ld_denom, amount, }], @@ -925,23 +986,11 @@ fn execute_unbond( Ok(response("execute-unbond", CONTRACT_NAME, attrs).add_messages(msgs)) } -fn get_unbonded_batch(deps: Deps) -> ContractResult> { - let batch_id = UNBOND_BATCH_ID.load(deps.storage)?; - if batch_id == 0 { - return Ok(None); - } - let batch = unbond_batches_map().load(deps.storage, batch_id - 1)?; - if batch.status == UnbondBatchStatus::Unbonded { - return Ok(Some((batch_id - 1, batch))); - } - Ok(None) -} - fn get_transfer_pending_balance_msg( deps: Deps, env: &Env, config: &Config, - funds: Vec, + funds: Vec, ) -> ContractResult, Uint128)>> { let pending_amount = deps .querier @@ -953,7 +1002,7 @@ fn get_transfer_pending_balance_msg( if pending_amount.is_zero() { return Ok(None); } - let mut all_funds = vec![cosmwasm_std::Coin { + let mut all_funds = vec![Coin { denom: config.base_denom.to_string(), amount: pending_amount, }]; @@ -979,16 +1028,19 @@ pub fn get_stake_msg( deps: DepsMut, env: &Env, config: &Config, - funds: Vec, -) -> ContractResult> { + funds: Vec, +) -> ContractResult>> { let (balance, balance_height, _) = get_ica_balance_by_denom( deps.as_ref(), &config.puppeteer_contract, &config.remote_denom, - false, + true, )?; - ensure_ne!(balance, Uint128::zero(), ContractError::ICABalanceZero {}); + if balance < config.min_stake_amount { + return Ok(None); + } + let last_ica_balance_change_height = LAST_ICA_BALANCE_CHANGE_HEIGHT.load(deps.storage)?; ensure!( last_ica_balance_change_height <= balance_height, @@ -1023,7 +1075,7 @@ pub fn get_stake_msg( })?; }; - Ok(CosmosMsg::Wasm(WasmMsg::Execute { + Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.puppeteer_contract.to_string(), msg: to_json_binary(&drop_staking_base::msg::puppeteer::ExecuteMsg::Delegate { items: to_delegate @@ -1034,7 +1086,7 @@ pub fn get_stake_msg( reply_to: env.contract.address.to_string(), })?, funds, - })) + }))) } fn get_received_puppeteer_response( @@ -1046,19 +1098,17 @@ fn get_received_puppeteer_response( } fn is_unbonding_time_close( - deps: Deps, + unbonding_batches: &[(u128, UnbondBatch)], now: u64, safe_period: u64, -) -> ContractResult { - let mut unbond_batch_id = UNBOND_BATCH_ID.load(deps.storage)?; - while unbond_batch_id > 0 { - let unbond_batch = unbond_batches_map().load(deps.storage, unbond_batch_id)?; - if unbond_batch.status == UnbondBatchStatus::Unbonding { - return Ok(now - unbond_batch.expected_release < safe_period); +) -> bool { + for (_id, unbond_batch) in unbonding_batches { + let expected = unbond_batch.expected_release; + if (now < expected) && (now > expected - safe_period) { + return true; } - unbond_batch_id -= 1; } - Ok(false) + false } fn get_ica_balance_by_denom( diff --git a/contracts/core/src/error.rs b/contracts/core/src/error.rs index c7decadf..7818768b 100644 --- a/contracts/core/src/error.rs +++ b/contracts/core/src/error.rs @@ -50,6 +50,9 @@ pub enum ContractError { #[error("Pump address is not set")] PumpAddressIsNotSet {}, + #[error("Emergency address is not set")] + EmergencyAddressIsNotSet {}, + #[error("InvalidTransaction")] InvalidTransaction {}, diff --git a/contracts/core/src/tests.rs b/contracts/core/src/tests.rs index 0733d8c1..e307326f 100644 --- a/contracts/core/src/tests.rs +++ b/contracts/core/src/tests.rs @@ -150,6 +150,8 @@ fn get_default_config(fee: Option) -> Config { fee_address: Some("fee_address".to_string()), lsm_redeem_threshold: 10u64, bond_limit: None, + emergency_address: None, + min_stake_amount: Uint128::new(100), } } @@ -281,6 +283,7 @@ fn get_stake_msg_success() { &get_default_config(Decimal::from_atomics(1u32, 1).ok()), vec![], ) + .unwrap() .unwrap(); assert_eq!( @@ -329,6 +332,7 @@ fn get_stake_msg_zero_fee() { &get_default_config(None), vec![], ) + .unwrap() .unwrap(); assert_eq!( diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs index df068579..65f4edfc 100644 --- a/contracts/factory/src/contract.rs +++ b/contracts/factory/src/contract.rs @@ -441,6 +441,8 @@ fn execute_init( owner: env.contract.address.to_string(), fee: None, fee_address: None, + emergency_address: None, + min_stake_amount: core_params.min_stake_amount, })?, funds: vec![], salt: Binary::from(salt), diff --git a/contracts/factory/src/msg.rs b/contracts/factory/src/msg.rs index b60dc51c..b84524fb 100644 --- a/contracts/factory/src/msg.rs +++ b/contracts/factory/src/msg.rs @@ -29,6 +29,7 @@ pub struct CoreParams { pub lsm_redeem_threshold: u64, pub channel: String, pub bond_limit: Option, + pub min_stake_amount: Uint128, } #[cw_serde] diff --git a/contracts/puppeteer/src/contract.rs b/contracts/puppeteer/src/contract.rs index 97f69322..697bfcd1 100644 --- a/contracts/puppeteer/src/contract.rs +++ b/contracts/puppeteer/src/contract.rs @@ -31,7 +31,7 @@ use drop_puppeteer_base::{ error::{ContractError, ContractResult}, msg::{ IBCTransferReason, QueryMsg, ReceiverExecuteMsg, ResponseAnswer, ResponseHookErrorMsg, - ResponseHookMsg, ResponseHookSuccessMsg, Transaction, TransferReadyBatchMsg, + ResponseHookMsg, ResponseHookSuccessMsg, Transaction, TransferReadyBatchesMsg, }, proto::MsgIBCTransfer, state::{ @@ -530,7 +530,7 @@ fn execute_claim_rewards_and_optionaly_transfer( mut deps: DepsMut, info: MessageInfo, validators: Vec, - transfer: Option, + transfer: Option, timeout: Option, reply_to: String, ) -> ContractResult> { diff --git a/integration_tests/package.json b/integration_tests/package.json index d97222f3..997e132c 100644 --- a/integration_tests/package.json +++ b/integration_tests/package.json @@ -5,18 +5,19 @@ "license": "MIT", "scripts": { "test": "vitest --run", - "test:poc-stargate": "vitest --run poc-stargate --bail 1", - "test:poc-provider-proposals": "vitest --run poc-provider-proposals.test --bail 1", - "test:poc-proposal-votes": "vitest --run poc-proposal-votes.test --bail 1", + "test:poc-stargate": "vitest --run poc-stargate.test.ts --bail 1", + "test:poc-provider-proposals": "vitest --run poc-provider-proposals.test.ts --bail 1", + "test:poc-proposal-votes": "vitest --run poc-proposal-votes.test.ts --bail 1", "test:core": "vitest --run core.test.ts --bail 1", + "test:core:slashing": "vitest --run core.slashing.test.ts --bail 1", "test:pump": "vitest --run pump.test.ts --bail 1", - "test:pump-multi": "vitest --run pump-multi --bail 1", - "test:puppeteer": "vitest --run puppeteer.test --bail 1", - "test:puppeteer-authz": "vitest --run puppeteer-authz.test --bail 1", - "test:validators-stats": "vitest --run validators-stats.test --bail 1", - "test:validator-set": "vitest --run validator-set.test --bail 1", - "test:distribution": "vitest --run distribution.test --bail 1", - "test:auto-withdrawer": "vitest --run auto-withdrawer.test --bail 1", + "test:pump-multi": "vitest --run pump-multi.test.ts --bail 1", + "test:puppeteer": "vitest --run puppeteer.test.ts --bail 1", + "test:puppeteer-authz": "vitest --run puppeteer-authz.test.ts --bail 1", + "test:validators-stats": "vitest --run validators-stats.test.ts --bail 1", + "test:validator-set": "vitest --run validator-set.test.ts --bail 1", + "test:distribution": "vitest --run distribution.test.ts --bail 1", + "test:auto-withdrawer": "vitest --run auto-withdrawer.test.ts --bail 1", "test:debug": "vitest --inspect-brk --bail 1 --threads false --run core.test.ts", "watch": "vitest", "build-ts-client": "ts-node ./src/rebuild-client.ts", diff --git a/integration_tests/src/generated/contractLib/dropCore.ts b/integration_tests/src/generated/contractLib/dropCore.ts index 800abd6f..a0093ef9 100644 --- a/integration_tests/src/generated/contractLib/dropCore.ts +++ b/integration_tests/src/generated/contractLib/dropCore.ts @@ -25,10 +25,12 @@ export interface InstantiateMsg { base_denom: string; bond_limit?: Uint128 | null; channel: string; + emergency_address?: string | null; fee?: Decimal | null; fee_address?: string | null; idle_min_interval: number; lsm_redeem_threshold: number; + min_stake_amount: Uint128; owner: string; pump_address?: string | null; puppeteer_contract: string; @@ -161,7 +163,7 @@ export type Transaction = claim_rewards_and_optionaly_transfer: { denom: string; interchain_account_id: string; - transfer?: TransferReadyBatchMsg | null; + transfer?: TransferReadyBatchesMsg | null; validators: string[]; }; } @@ -169,6 +171,7 @@ export type Transaction = i_b_c_transfer: { amount: number; denom: string; + reason: IBCTransferReason; recipient: string; }; } @@ -178,6 +181,7 @@ export type Transaction = items: [string, Coin][]; }; }; +export type IBCTransferReason = "l_s_m_share" | "stake"; export type ArrayOfNonNativeRewardsItem = NonNativeRewardsItem[]; export type String = string; export type ArrayOfTupleOf_StringAnd_TupleOf_StringAnd_Uint1281 = [string, [string, Uint128]][]; @@ -200,9 +204,10 @@ export type UnbondBatchStatus = | "unbond_requested" | "unbond_failed" | "unbonding" - | "unbonded" | "withdrawing" - | "withdrawn"; + | "withdrawn" + | "withdrawing_emergency" + | "withdrawn_emergency"; export type PuppeteerHookArgs = | { success: ResponseHookSuccessMsg; @@ -280,11 +285,13 @@ export interface Config { base_denom: string; bond_limit?: Uint128 | null; channel: string; + emergency_address?: string | null; fee?: Decimal | null; fee_address?: string | null; idle_min_interval: number; ld_denom?: string | null; lsm_redeem_threshold: number; + min_stake_amount: Uint128; pump_address?: string | null; puppeteer_contract: string; puppeteer_timeout: number; @@ -352,9 +359,10 @@ export interface RedeemShareItem { local_denom: string; remote_denom: string; } -export interface TransferReadyBatchMsg { +export interface TransferReadyBatchesMsg { amount: Uint128; - batch_id: number; + batch_ids: number[]; + emergency: boolean; recipient: string; } export interface ResponseHookErrorMsg { @@ -399,11 +407,13 @@ export interface ConfigOptional { base_denom?: string | null; bond_limit?: Uint128 | null; channel?: string | null; + emergency_address?: string | null; fee?: Decimal | null; fee_address?: string | null; idle_min_interval?: number | null; ld_denom?: string | null; lsm_redeem_threshold?: number | null; + min_stake_amount?: Uint128 | null; pump_address?: string | null; puppeteer_contract?: string | null; puppeteer_timeout?: number | null; diff --git a/integration_tests/src/generated/contractLib/dropFactory.ts b/integration_tests/src/generated/contractLib/dropFactory.ts index 36d03458..5fe2ccfe 100644 --- a/integration_tests/src/generated/contractLib/dropFactory.ts +++ b/integration_tests/src/generated/contractLib/dropFactory.ts @@ -204,6 +204,7 @@ export interface CoreParams { channel: string; idle_min_interval: number; lsm_redeem_threshold: number; + min_stake_amount: Uint128; puppeteer_timeout: number; unbond_batch_switch_time: number; unbonding_period: number; @@ -213,11 +214,13 @@ export interface ConfigOptional { base_denom?: string | null; bond_limit?: Uint128 | null; channel?: string | null; + emergency_address?: string | null; fee?: Decimal | null; fee_address?: string | null; idle_min_interval?: number | null; ld_denom?: string | null; lsm_redeem_threshold?: number | null; + min_stake_amount?: Uint128 | null; pump_address?: string | null; puppeteer_contract?: string | null; puppeteer_timeout?: number | null; diff --git a/integration_tests/src/generated/contractLib/dropHookTester.ts b/integration_tests/src/generated/contractLib/dropHookTester.ts index 9e16a4b8..b41f0175 100644 --- a/integration_tests/src/generated/contractLib/dropHookTester.ts +++ b/integration_tests/src/generated/contractLib/dropHookTester.ts @@ -98,7 +98,7 @@ export type Transaction = claim_rewards_and_optionaly_transfer: { denom: string; interchain_account_id: string; - transfer?: TransferReadyBatchMsg | null; + transfer?: TransferReadyBatchesMsg | null; validators: string[]; }; } @@ -106,6 +106,7 @@ export type Transaction = i_b_c_transfer: { amount: number; denom: string; + reason: IBCTransferReason; recipient: string; }; } @@ -115,6 +116,7 @@ export type Transaction = items: [string, Coin][]; }; }; +export type IBCTransferReason = "l_s_m_share" | "stake"; export type ArrayOfResponseHookSuccessMsg = ResponseHookSuccessMsg[]; export type ArrayOfResponseHookErrorMsg = ResponseHookErrorMsg[]; export type PuppeteerHookArgs = @@ -191,9 +193,10 @@ export interface RedeemShareItem { local_denom: string; remote_denom: string; } -export interface TransferReadyBatchMsg { +export interface TransferReadyBatchesMsg { amount: Uint128; - batch_id: number; + batch_ids: number[]; + emergency: boolean; recipient: string; } export interface ResponseHookErrorMsg { diff --git a/integration_tests/src/generated/contractLib/dropPuppeteer.ts b/integration_tests/src/generated/contractLib/dropPuppeteer.ts index 758a93eb..b3580d5f 100644 --- a/integration_tests/src/generated/contractLib/dropPuppeteer.ts +++ b/integration_tests/src/generated/contractLib/dropPuppeteer.ts @@ -72,7 +72,7 @@ export type Transaction = claim_rewards_and_optionaly_transfer: { denom: string; interchain_account_id: string; - transfer?: TransferReadyBatchMsg | null; + transfer?: TransferReadyBatchesMsg | null; validators: string[]; }; } @@ -80,6 +80,7 @@ export type Transaction = i_b_c_transfer: { amount: number; denom: string; + reason: IBCTransferReason; recipient: string; }; } @@ -103,6 +104,7 @@ export type Transaction = * let c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ``` */ export type Uint128 = string; +export type IBCTransferReason = "l_s_m_share" | "stake"; export type ArrayOfTransaction = Transaction[]; export type QueryExtMsg = | { @@ -149,9 +151,10 @@ export interface RedeemShareItem { local_denom: string; remote_denom: string; } -export interface TransferReadyBatchMsg { +export interface TransferReadyBatchesMsg { amount: Uint128; - batch_id: number; + batch_ids: number[]; + emergency: boolean; recipient: string; } export interface Coin { @@ -207,6 +210,7 @@ export interface RedeemSharesArgs { timeout?: number | null; } export interface IBCTransferArgs { + reason: IBCTransferReason; reply_to: string; timeout: number; } @@ -218,7 +222,7 @@ export interface TransferArgs { export interface ClaimRewardsAndOptionalyTransferArgs { reply_to: string; timeout?: number | null; - transfer?: TransferReadyBatchMsg | null; + transfer?: TransferReadyBatchesMsg | null; validators: string[]; } diff --git a/integration_tests/src/testSuite.ts b/integration_tests/src/testSuite.ts index dfe3b29a..6bf20f68 100644 --- a/integration_tests/src/testSuite.ts +++ b/integration_tests/src/testSuite.ts @@ -74,10 +74,12 @@ const networkConfigs = { trace: true, validators: 2, commands: redefinedParams.commands, - validators_balance: '1000000000', + validators_balance: ['1900000000', '100000000'], genesis_opts: redefinedParams.genesisOpts || { 'app_state.slashing.params.downtime_jail_duration': '10s', 'app_state.slashing.params.signed_blocks_window': '10', + 'app_state.slashing.params.min_signed_per_window': '0.9', + 'app_state.slashing.params.slash_fraction_downtime': '0.1', 'app_state.staking.params.validator_bond_factor': '10', 'app_state.staking.params.unbonding_time': '1814400s', 'app_state.mint.minter.inflation': '0.9', diff --git a/integration_tests/src/testcases/auto-withdrawer.test.ts b/integration_tests/src/testcases/auto-withdrawer.test.ts index 9b7daccb..79c3ea17 100644 --- a/integration_tests/src/testcases/auto-withdrawer.test.ts +++ b/integration_tests/src/testcases/auto-withdrawer.test.ts @@ -412,6 +412,7 @@ describe('Auto withdrawer', () => { unbonding_period: UNBONDING_TIME, channel: 'channel-0', lsm_redeem_threshold: 10, + min_stake_amount: '2', }, }); expect(res.transactionHash).toHaveLength(64); diff --git a/integration_tests/src/testcases/core-slashing.test.ts b/integration_tests/src/testcases/core-slashing.test.ts new file mode 100644 index 00000000..eeff1a75 --- /dev/null +++ b/integration_tests/src/testcases/core-slashing.test.ts @@ -0,0 +1,1161 @@ +import { describe, expect, it, beforeAll, afterAll } from 'vitest'; +import { + DropCore, + DropFactory, + DropPump, + DropPuppeteer, + DropStrategy, + DropWithdrawalManager, + DropWithdrawalVoucher, +} from '../generated/contractLib'; +import { + QueryClient, + StakingExtension, + BankExtension, + setupStakingExtension, + setupBankExtension, + setupSlashingExtension, + SigningStargateClient, +} from '@cosmjs/stargate'; +import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx'; +import { join } from 'path'; +import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; +import { Client as NeutronClient } from '@neutron-org/client-ts'; +import { AccountData, DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; +import { GasPrice } from '@cosmjs/stargate'; +import { setupPark } from '../testSuite'; +import fs from 'fs'; +import Cosmopark from '@neutron-org/cosmopark'; +import { waitFor } from '../helpers/waitFor'; +import { UnbondBatch } from '../generated/contractLib/dropCore'; +import { stringToPath } from '@cosmjs/crypto'; +import { sleep } from '../helpers/sleep'; +import dockerCompose from 'docker-compose'; +import { SlashingExtension } from '@cosmjs/stargate/build/modules'; + +const DropFactoryClass = DropFactory.Client; +const DropCoreClass = DropCore.Client; +const DropPumpClass = DropPump.Client; +const DropPuppeteerClass = DropPuppeteer.Client; +const DropStrategyClass = DropStrategy.Client; +const DropWithdrawalVoucherClass = DropWithdrawalVoucher.Client; +const DropWithdrawalManagerClass = DropWithdrawalManager.Client; +const UNBONDING_TIME = 360; + +describe('Core Slashing', () => { + const context: { + park?: Cosmopark; + contractAddress?: string; + wallet?: DirectSecp256k1HdWallet; + gaiaWallet?: DirectSecp256k1HdWallet; + gaiaWallet2?: DirectSecp256k1HdWallet; + factoryContractClient?: InstanceType; + coreContractClient?: InstanceType; + strategyContractClient?: InstanceType; + pumpContractClient?: InstanceType; + puppeteerContractClient?: InstanceType; + withdrawalVoucherContractClient?: InstanceType< + typeof DropWithdrawalVoucherClass + >; + withdrawalManagerContractClient?: InstanceType< + typeof DropWithdrawalManagerClass + >; + account?: AccountData; + icaAddress?: string; + client?: SigningCosmWasmClient; + gaiaClient?: SigningStargateClient; + gaiaUserAddress?: string; + gaiaUserAddress2?: string; + gaiaQueryClient?: QueryClient & + StakingExtension & + SlashingExtension & + BankExtension; + neutronClient?: InstanceType; + neutronUserAddress?: string; + neutronSecondUserAddress?: string; + validatorAddress?: string; + secondValidatorAddress?: string; + tokenizedDenomOnNeutron?: string; + codeIds: { + core?: number; + token?: number; + withdrawalVoucher?: number; + withdrawalManager?: number; + strategy?: number; + puppeteer?: number; + validatorsSet?: number; + distribution?: number; + rewardsManager?: number; + }; + tokenContractAddress?: string; + neutronIBCDenom?: string; + } = { codeIds: {} }; + + beforeAll(async (t) => { + context.park = await setupPark( + t, + ['neutron', 'gaia'], + { + gaia: { + genesis_opts: { + 'app_state.staking.params.unbonding_time': `${UNBONDING_TIME}s`, + }, + }, + }, + { + neutron: true, + hermes: { + config: { + 'chains.1.trusting_period': '2m0s', + 'chains.1.unbonding_period': `${UNBONDING_TIME}s`, + }, + }, + }, + ); + context.wallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.wallets.demowallet1.mnemonic, + { + prefix: 'neutron', + }, + ); + context.gaiaWallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.wallets.demowallet1.mnemonic, + { + prefix: 'cosmos', + }, + ); + context.gaiaWallet2 = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.wallets.demo1.mnemonic, + { + prefix: 'cosmos', + }, + ); + context.account = (await context.wallet.getAccounts())[0]; + context.neutronClient = new NeutronClient({ + apiURL: `http://127.0.0.1:${context.park.ports.neutron.rest}`, + rpcURL: `127.0.0.1:${context.park.ports.neutron.rpc}`, + prefix: 'neutron', + }); + context.client = await SigningCosmWasmClient.connectWithSigner( + `http://127.0.0.1:${context.park.ports.neutron.rpc}`, + context.wallet, + { + gasPrice: GasPrice.fromString('0.025untrn'), + }, + ); + context.gaiaClient = await SigningStargateClient.connectWithSigner( + `http://127.0.0.1:${context.park.ports.gaia.rpc}`, + context.gaiaWallet, + { + gasPrice: GasPrice.fromString('0.025stake'), + }, + ); + const tmClient = await Tendermint34Client.connect( + `http://127.0.0.1:${context.park.ports.gaia.rpc}`, + ); + context.gaiaQueryClient = QueryClient.withExtensions( + tmClient, + setupStakingExtension, + setupSlashingExtension, + setupBankExtension, + ); + const secondWallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.wallets.demo2.mnemonic, + { + prefix: 'neutron', + }, + ); + context.neutronSecondUserAddress = ( + await secondWallet.getAccounts() + )[0].address; + }); + + afterAll(async () => { + await context.park.stop(); + }); + + it('instantiate', async () => { + const { client, account } = context; + context.codeIds = {}; + + { + const res = await client.upload( + account.address, + fs.readFileSync(join(__dirname, '../../../artifacts/drop_core.wasm')), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.core = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync(join(__dirname, '../../../artifacts/drop_token.wasm')), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.token = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_withdrawal_voucher.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.withdrawalVoucher = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_withdrawal_manager.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.withdrawalManager = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_strategy.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.strategy = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_distribution.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.distribution = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_validators_set.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.validatorsSet = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_puppeteer.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.puppeteer = res.codeId; + } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_rewards_manager.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.rewardsManager = res.codeId; + } + + const res = await client.upload( + account.address, + fs.readFileSync(join(__dirname, '../../../artifacts/drop_factory.wasm')), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + const instantiateRes = await DropFactory.Client.instantiate( + client, + account.address, + res.codeId, + { + sdk_version: process.env.SDK_VERSION || '0.46.0', + code_ids: { + core_code_id: context.codeIds.core, + token_code_id: context.codeIds.token, + withdrawal_voucher_code_id: context.codeIds.withdrawalVoucher, + withdrawal_manager_code_id: context.codeIds.withdrawalManager, + strategy_code_id: context.codeIds.strategy, + distribution_code_id: context.codeIds.distribution, + validators_set_code_id: context.codeIds.validatorsSet, + puppeteer_code_id: context.codeIds.puppeteer, + rewards_manager_code_id: context.codeIds.rewardsManager, + }, + remote_opts: { + connection_id: 'connection-0', + transfer_channel_id: 'channel-0', + port_id: 'transfer', + denom: 'stake', + update_period: 2, + }, + salt: 'salt', + subdenom: 'drop', + token_metadata: { + description: 'Drop token', + display: 'drop', + exponent: 6, + name: 'Drop liquid staking token', + symbol: 'DROP', + uri: null, + uri_hash: null, + }, + }, + 'drop-staking-factory', + [], + 'auto', + ); + expect(instantiateRes.contractAddress).toHaveLength(66); + context.contractAddress = instantiateRes.contractAddress; + context.factoryContractClient = new DropFactory.Client( + client, + context.contractAddress, + ); + context.gaiaUserAddress = ( + await context.gaiaWallet.getAccounts() + )[0].address; + context.gaiaUserAddress2 = ( + await context.gaiaWallet2.getAccounts() + )[0].address; + context.neutronUserAddress = ( + await context.wallet.getAccounts() + )[0].address; + { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.master_mnemonic, + { + prefix: 'cosmosvaloper', + hdPaths: [stringToPath("m/44'/118'/1'/0/0") as any], + }, + ); + context.validatorAddress = (await wallet.getAccounts())[0].address; + } + { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + context.park.config.master_mnemonic, + { + prefix: 'cosmosvaloper', + hdPaths: [stringToPath("m/44'/118'/2'/0/0") as any], + }, + ); + context.secondValidatorAddress = (await wallet.getAccounts())[0].address; + } + }); + it('transfer tokens to neutron', async () => { + const { gaiaClient, gaiaUserAddress, neutronUserAddress, neutronClient } = + context; + const res = await gaiaClient.signAndBroadcast( + gaiaUserAddress, + [ + { + typeUrl: '/ibc.applications.transfer.v1.MsgTransfer', + value: MsgTransfer.fromPartial({ + sender: gaiaUserAddress, + sourceChannel: 'channel-0', + sourcePort: 'transfer', + receiver: neutronUserAddress, + token: { denom: 'stake', amount: '2000000' }, + timeoutTimestamp: BigInt((Date.now() + 10 * 60 * 1000) * 1e6), + timeoutHeight: { + revisionHeight: BigInt(0), + revisionNumber: BigInt(0), + }, + }), + }, + ], + 1.5, + ); + expect(res.transactionHash).toHaveLength(64); + await waitFor(async () => { + const balances = + await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronUserAddress, + ); + context.neutronIBCDenom = balances.data.balances.find((b) => + b.denom.startsWith('ibc/'), + )?.denom; + return balances.data.balances.length > 1; + }); + expect(context.neutronIBCDenom).toBeTruthy(); + }); + it('init', async () => { + const { factoryContractClient: contractClient } = context; + await contractClient.init(context.neutronUserAddress, { + base_denom: context.neutronIBCDenom, + core_params: { + idle_min_interval: 10, + puppeteer_timeout: 60, + unbond_batch_switch_time: 240, + unbonding_safe_period: 10, + unbonding_period: 360, + channel: 'channel-0', + lsm_redeem_threshold: 2, + bond_limit: '0', + min_stake_amount: '2', + }, + }); + const res = await contractClient.queryState(); + context.coreContractClient = new DropCore.Client( + context.client, + res.core_contract, + ); + context.withdrawalVoucherContractClient = new DropWithdrawalVoucher.Client( + context.client, + res.withdrawal_voucher_contract, + ); + context.withdrawalManagerContractClient = new DropWithdrawalManager.Client( + context.client, + res.withdrawal_manager_contract, + ); + context.strategyContractClient = new DropStrategy.Client( + context.client, + res.strategy_contract, + ); + context.tokenContractAddress = res.token_contract; + context.puppeteerContractClient = new DropPuppeteer.Client( + context.client, + res.puppeteer_contract, + ); + }); + it('set fees for puppeteer', async () => { + const { neutronUserAddress, factoryContractClient: contractClient } = + context; + await contractClient.updateConfig(neutronUserAddress, { + puppeteer_fees: { + timeout_fee: '10000', + ack_fee: '10000', + recv_fee: '0', + register_fee: '1000000', + }, + }); + }); + it('register ICA', async () => { + const { puppeteerContractClient, neutronUserAddress } = context; + const res = await puppeteerContractClient.registerICA( + neutronUserAddress, + 1.5, + undefined, + [{ amount: '1000000', denom: 'untrn' }], + ); + expect(res.transactionHash).toHaveLength(64); + let ica = ''; + await waitFor(async () => { + const res = await puppeteerContractClient.queryIca(); + switch (res) { + case 'none': + case 'in_progress': + case 'timeout': + return false; + default: + ica = res.registered.ica_address; + return true; + } + }, 100_000); + expect(ica).toHaveLength(65); + expect(ica.startsWith('cosmos')).toBeTruthy(); + context.icaAddress = ica; + }); + it('add validators into validators set', async () => { + const { + neutronUserAddress, + factoryContractClient, + validatorAddress, + secondValidatorAddress, + } = context; + await factoryContractClient.proxy( + neutronUserAddress, + { + validator_set: { + update_validators: { + validators: [ + { + valoper_address: validatorAddress, + weight: 1, + }, + { + valoper_address: secondValidatorAddress, + weight: 1, + }, + ], + }, + }, + }, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + }); + it('set emergency address', async () => { + const { neutronUserAddress, factoryContractClient } = context; + await factoryContractClient.updateConfig( + neutronUserAddress, + { + core: { + emergency_address: 'cosmos1tqchhqtug30lmz9y6zltdp7cmyctnkshm850rz', + }, + }, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + }); + it('deploy pump', async () => { + const { client, account, neutronUserAddress } = context; + const resUpload = await client.upload( + account.address, + fs.readFileSync(join(__dirname, '../../../artifacts/drop_pump.wasm')), + 1.5, + ); + expect(resUpload.codeId).toBeGreaterThan(0); + const { codeId } = resUpload; + const res = await DropPump.Client.instantiate( + client, + neutronUserAddress, + codeId, + { + connection_id: 'connection-0', + ibc_fees: { + timeout_fee: '10000', + ack_fee: '10000', + recv_fee: '0', + register_fee: '1000000', + }, + local_denom: 'untrn', + timeout: { + local: 60, + remote: 60, + }, + dest_address: context.withdrawalManagerContractClient.contractAddress, + dest_port: 'transfer', + dest_channel: 'channel-0', + refundee: neutronUserAddress, + owner: account.address, + }, + 'Drop-staking-pump', + [], + 1.5, + ); + expect(res.contractAddress).toHaveLength(66); + context.pumpContractClient = new DropPump.Client( + client, + res.contractAddress, + ); + await context.pumpContractClient.registerICA( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + let ica = ''; + await waitFor(async () => { + const res = await context.pumpContractClient.queryIca(); + switch (res) { + case 'none': + case 'in_progress': + case 'timeout': + return false; + default: + ica = res.registered.ica_address; + return true; + } + }, 50_000); + expect(ica).toHaveLength(65); + expect(ica.startsWith('cosmos')).toBeTruthy(); + const resFactory = await context.factoryContractClient.updateConfig( + neutronUserAddress, + { + core: { + pump_address: ica, + }, + }, + ); + expect(resFactory.transactionHash).toHaveLength(64); + }); + + describe('prepare unbonding batch 0', () => { + it('bond', async () => { + const { coreContractClient, neutronUserAddress, neutronIBCDenom } = + context; + await coreContractClient.bond(neutronUserAddress, {}, 1.6, undefined, [ + { + amount: '1000', + denom: neutronIBCDenom, + }, + ]); + }); + it('unbond', async () => { + const { coreContractClient, neutronUserAddress } = context; + await coreContractClient.unbond(neutronUserAddress, 1.6, undefined, [ + { + amount: '1000', + denom: `factory/${context.tokenContractAddress}/drop`, + }, + ]); + }); + it('tick 1 (transfering)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('transfering'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + let res; + await waitFor(async () => { + try { + res = await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + }); + } catch (e) { + // + } + return res && res[0].coins.length !== 0; + }, 100_000); + }); + it('tick 2 (staking)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + await waitFor(async () => { + try { + await context.strategyContractClient.queryCalcWithdraw({ + withdraw: '1000', + }); + return true; + } catch (e) { + return false; + } + }, 100_000); + }); + it('tick 3 (unbonding)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('unbonding'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('tick 4 (idle)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + await sleep(10_000); // wait for idle min interval + }); + it('verify that unbonding batch 0 is in unbonding state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'unbonding', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '1000', + expected_amount: '1000', + unbond_items: [ + { + amount: '1000', + expected_amount: '1000', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + }); + + describe('prepare unbonding batch 1', () => { + it('bond', async () => { + const { coreContractClient, neutronUserAddress, neutronIBCDenom } = + context; + await coreContractClient.bond(neutronUserAddress, {}, 1.6, undefined, [ + { + amount: '3000', + denom: neutronIBCDenom, + }, + ]); + }); + it('unbond', async () => { + const { coreContractClient, neutronUserAddress } = context; + await coreContractClient.unbond(neutronUserAddress, 1.6, undefined, [ + { + amount: '3000', + denom: `factory/${context.tokenContractAddress}/drop`, + }, + ]); + }); + it('tick 1 (claiming)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('claiming'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 30_000); + }); + it('tick 2 (transfering)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('transfering'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 100_000); + }); + it('tick 3 (staking)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('staking'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + await waitFor(async () => { + try { + await context.strategyContractClient.queryCalcWithdraw({ + withdraw: '3000', + }); + return true; + } catch (e) { + return false; + } + }, 100_000); + }); + it('tick 4 (unbonding)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [ + { + amount: '1000000', + denom: 'untrn', + }, + ], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('unbonding'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + }); + it('tick 5 (idle)', async () => { + const { neutronUserAddress } = context; + await context.coreContractClient.tick( + neutronUserAddress, + 1.5, + undefined, + [], + ); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + await sleep(10_000); // wait for idle min interval + }); + it('verify that unbonding batch 1 is in unbonding state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '1', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'unbonding', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '3000', + expected_amount: '3499', + unbond_items: [ + { + amount: '3000', + expected_amount: '3499', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + }); + + it('trigger slashing', async () => { + await dockerCompose.pauseOne('gaia_val2'); + await waitFor(async () => { + const signingInfos = + await context.gaiaQueryClient.slashing.signingInfos(); + const v1 = signingInfos.info[0]; + const v2 = signingInfos.info[1]; + return v1.jailedUntil.seconds > 0 || v2.jailedUntil.seconds > 0; + }, 60_000); + }); + it('wait until unbonding period for unbonding batch 0 is finished', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + const currentTime = Math.floor(Date.now() / 1000); + if (batchInfo.expected_release > currentTime) { + const diffMs = (batchInfo.expected_release - currentTime + 1) * 1000; + await sleep(diffMs); + } + }); + it('wait until unbonding period for unbonding batch 1 is finished', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '1', + }); + const currentTime = Math.floor(Date.now() / 1000); + if (batchInfo.expected_release > currentTime) { + const diffMs = (batchInfo.expected_release - currentTime + 1) * 1000; + await sleep(diffMs); + } + }); + it('wait until fresh ICA balance for unbonding batch 0 is delivered', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + await waitFor(async () => { + const icaTs = Math.floor( + ( + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any + )[2] / 1e9, + ); + return icaTs > batchInfo.expected_release; + }, 50_000); + }); + it('wait until fresh ICA balance for unbonding batch 1 is delivered', async () => { + const batchInfo = await context.coreContractClient.queryUnbondBatch({ + batch_id: '1', + }); + await waitFor(async () => { + const icaTs = Math.floor( + ( + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any + )[2] / 1e9, + ); + return icaTs > batchInfo.expected_release; + }, 50_000); + }); + it('tick (claiming)', async () => { + const { coreContractClient, neutronUserAddress } = context; + await coreContractClient.tick(neutronUserAddress, 1.5, undefined, []); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('claiming'); + let response; + await waitFor(async () => { + try { + response = + await context.coreContractClient.queryLastPuppeteerResponse(); + } catch (e) { + // + } + return !!response; + }, 100_000); + const [, currentHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + await waitFor(async () => { + const [, nowHeight] = + (await context.puppeteerContractClient.queryExtention({ + msg: { + balances: {}, + }, + })) as any; + return nowHeight !== currentHeight; + }, 30_000); + }); + it('verify that unbonding batch 0 is in withdrawing emergency state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'withdrawing_emergency', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '1000', + expected_amount: '1000', + unbond_items: [ + { + amount: '1000', + expected_amount: '1000', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + it('verify that unbonding batch 1 is in withdrawing emergency state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '1', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'withdrawing_emergency', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '3000', + expected_amount: '3499', + unbond_items: [ + { + amount: '3000', + expected_amount: '3499', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + it('tick (idle)', async () => { + const { coreContractClient, neutronUserAddress } = context; + await coreContractClient.tick(neutronUserAddress, 1.5, undefined, []); + const state = await context.coreContractClient.queryContractState(); + expect(state).toEqual('idle'); + }); + it('verify that unbonding batch 0 is in withdrawn emergency state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'withdrawn_emergency', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '1000', + expected_amount: '1000', + unbond_items: [ + { + amount: '1000', + expected_amount: '1000', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + it('verify that unbonding batch 1 is in withdrawn emergency state', async () => { + const batch = await context.coreContractClient.queryUnbondBatch({ + batch_id: '1', + }); + expect(batch).toBeTruthy(); + expect(batch).toEqual({ + slashing_effect: null, + status: 'withdrawn_emergency', + created: expect.any(Number), + expected_release: expect.any(Number), + total_amount: '3000', + expected_amount: '3499', + unbond_items: [ + { + amount: '3000', + expected_amount: '3499', + sender: context.neutronUserAddress, + }, + ], + unbonded_amount: null, + withdrawed_amount: null, + }); + }); + + it('verify that emergency account has received unbonded funds', async () => { + const emergencyBalance = parseInt( + ( + await context.gaiaClient.getBalance( + 'cosmos1tqchhqtug30lmz9y6zltdp7cmyctnkshm850rz', + 'stake', + ) + ).amount, + ); + expect(emergencyBalance).toBeGreaterThan(0); + expect(emergencyBalance).toBeLessThan(1000 + 3499); + }); +}); diff --git a/integration_tests/src/testcases/core.test.ts b/integration_tests/src/testcases/core.test.ts index c53a62f5..3f9b8570 100644 --- a/integration_tests/src/testcases/core.test.ts +++ b/integration_tests/src/testcases/core.test.ts @@ -413,6 +413,7 @@ describe('Core', () => { channel: 'channel-0', lsm_redeem_threshold: 2, bond_limit: '100000', + min_stake_amount: '2', }, }); expect(res.transactionHash).toHaveLength(64); diff --git a/integration_tests/src/testcases/pump.test.ts b/integration_tests/src/testcases/pump.test.ts index 92f4640a..54f43407 100644 --- a/integration_tests/src/testcases/pump.test.ts +++ b/integration_tests/src/testcases/pump.test.ts @@ -289,6 +289,13 @@ describe('Pump', () => { }); it('check balance on pump', async () => { const { neutronClient, contractAddress } = context; + await waitFor(async () => { + const res = + await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + contractAddress, + ); + return res.data.balances.length > 0; + }); const res = await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( contractAddress, diff --git a/integration_tests/test.json b/integration_tests/test.json deleted file mode 100644 index 8dad5df1..00000000 --- a/integration_tests/test.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "context": "first", - "networks": { - "neutron": { - "binary": "neutrond", - "chain_id": "nnn", - "denom": "untrn", - "image": "neutron-node", - "prefix": "neutron", - "type": "ics", - "upload": [ - "./artifacts/contracts", - "./artifacts/contracts_thirdparty", - "./artifacts/init-neutrond.sh" - ], - "post_init": [ - "CHAINID=nnn CHAIN_DIR=/opt /opt/artifacts/init-neutrond.sh" - ], - "genesis_opts": { - "app_state.crisis.constant_fee.denom": "untrn" - }, - "config_opts": { - "consensus.timeout_commit": "1s", - "consensus.timeout_propose": "1s" - }, - "app_opts": { - "api.enable": "true", - "api.swagger": "true", - "grpc.enable": "true", - "minimum-gas-prices": "0.0025untrn", - "rosetta.enable": "true", - "telemetry.prometheus-retention-time": 1000 - } - }, - "lsm": { - "binary": "liquidstakingd", - "chain_id": "testlsm", - "denom": "stake", - "image": "lsm", - "prefix": "cosmos", - "validators": 1, - "validators_balance": "1000000000", - "genesis_opts": { - "app_state.slashing.params.downtime_jail_duration": "10s", - "app_state.slashing.params.signed_blocks_window": "10", - "app_state.staking.params.validator_bond_factor": "10" - }, - "config_opts": { - "rpc.laddr": "tcp://0.0.0.0:26657" - }, - "app_opts": { - "api.enable": true, - "api.swagger": true, - "grpc.enable": true, - "minimum-gas-prices": "0stake", - "rosetta.enable": true - } - } - }, - "master_mnemonic": "drama disorder fall occur nut buyer portion diesel jazz floor success walnut", - "portOffset": 100, - "multicontext": true, - "wallets": { - "demowallet1": { - "mnemonic": "advice convince glide reveal uniform come staff bring tape upon light error", - "balance": "1000000000" - }, - "demo1": { - "mnemonic": "shield vote rain usual only valve label guess hotel pioneer faint stay", - "balance": "1000000000" - }, - "demo2": { - "mnemonic": "empty fringe forest jazz include invest volcano alley primary crucial shaft fence", - "balance": "1000000000" - }, - "demo3": { - "mnemonic": "shy gather ceiling option book install resist grow bag talent beauty similar", - "balance": "1000000000" - } - }, - "relayers": [ - { - "balance": "1000000000", - "binary": "hermes", - "config": { - "chains.0.trusting_period": "14days", - "chains.0.unbonding_period": "504h0m0s" - }, - "image": "hermes", - "log_level": "trace", - "type": "hermes", - "networks": [ - "neutron", - "lsm" - ], - "connections": [ - [ - "neutron", - "lsm" - ] - ], - "mnemonic": "episode girl steel circle census stock toddler else strong rescue magnet chuckle" - }, - { - "balance": "1000000000", - "binary": "neutron-query-relayer", - "image": "neutron-org/neutron-query-relayer", - "log_level": "info", - "type": "neutron", - "networks": [ - "neutron", - "lsm" - ], - "mnemonic": "second illness town carpet forest accident student ball topic fix tide lottery" - } - ] -} \ No newline at end of file diff --git a/packages/base/src/msg/core.rs b/packages/base/src/msg/core.rs index bab171a7..7f347e8e 100644 --- a/packages/base/src/msg/core.rs +++ b/packages/base/src/msg/core.rs @@ -26,6 +26,8 @@ pub struct InstantiateMsg { pub owner: String, pub fee: Option, pub fee_address: Option, + pub emergency_address: Option, + pub min_stake_amount: Uint128, } #[cw_serde] @@ -89,9 +91,15 @@ impl From for Config { lsm_redeem_threshold: val.lsm_redeem_threshold, validators_set_contract: val.validators_set_contract, unbond_batch_switch_time: val.unbond_batch_switch_time, - bond_limit: val.bond_limit, + bond_limit: match val.bond_limit { + None => None, + Some(limit) if limit.is_zero() => None, + Some(limit) => Some(limit), + }, fee: val.fee, fee_address: val.fee_address, + emergency_address: val.emergency_address, + min_stake_amount: val.min_stake_amount, } } } diff --git a/packages/base/src/msg/proposal_votes.rs b/packages/base/src/msg/proposal_votes.rs index 29226d67..a52ac79c 100644 --- a/packages/base/src/msg/proposal_votes.rs +++ b/packages/base/src/msg/proposal_votes.rs @@ -1,7 +1,6 @@ +use crate::state::proposal_votes::ConfigOptional; use cosmwasm_schema::{cw_serde, QueryResponses}; -use crate::state::proposal_votes::{Config, ConfigOptional, Metrics}; - #[cw_serde] pub struct InstantiateMsg { pub connection_id: String, @@ -21,9 +20,9 @@ pub enum ExecuteMsg { #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - #[returns(Config)] + #[returns(crate::state::proposal_votes::Config)] Config {}, - #[returns(Metrics)] + #[returns(crate::state::proposal_votes::Metrics)] Metrics {}, } diff --git a/packages/base/src/msg/provider_proposals.rs b/packages/base/src/msg/provider_proposals.rs index 1ddf0646..3abd2870 100644 --- a/packages/base/src/msg/provider_proposals.rs +++ b/packages/base/src/msg/provider_proposals.rs @@ -1,9 +1,8 @@ +use crate::state::provider_proposals::ConfigOptional; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Decimal; use neutron_sdk::interchain_queries::v045::types::ProposalVote; -use crate::state::provider_proposals::{Config, ConfigOptional, Metrics, ProposalInfo}; - #[cw_serde] pub struct InstantiateMsg { pub connection_id: String, @@ -25,13 +24,13 @@ pub enum ExecuteMsg { #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - #[returns(Config)] + #[returns(crate::state::provider_proposals::Config)] Config {}, - #[returns(ProposalInfo)] + #[returns(crate::state::provider_proposals::ProposalInfo)] GetProposal { proposal_id: u64 }, - #[returns(Vec)] + #[returns(Vec)] GetProposals {}, - #[returns(Metrics)] + #[returns(crate::state::provider_proposals::Metrics)] Metrics {}, } diff --git a/packages/base/src/msg/puppeteer.rs b/packages/base/src/msg/puppeteer.rs index 18796d4e..aad3e63c 100644 --- a/packages/base/src/msg/puppeteer.rs +++ b/packages/base/src/msg/puppeteer.rs @@ -9,7 +9,7 @@ use cosmos_sdk_proto::cosmos::{ staking::v1beta1::{Delegation, Params, Validator as CosmosValidator}, }; use drop_puppeteer_base::{ - msg::{ExecuteMsg as BaseExecuteMsg, IBCTransferReason, TransferReadyBatchMsg}, + msg::{ExecuteMsg as BaseExecuteMsg, IBCTransferReason, TransferReadyBatchesMsg}, r#trait::PuppeteerReconstruct, state::RedeemShareItem, }; @@ -97,7 +97,7 @@ pub enum ExecuteMsg { }, ClaimRewardsAndOptionalyTransfer { validators: Vec, - transfer: Option, + transfer: Option, timeout: Option, reply_to: String, }, diff --git a/packages/base/src/msg/validatorset.rs b/packages/base/src/msg/validatorset.rs index ed8e95c0..d6179455 100644 --- a/packages/base/src/msg/validatorset.rs +++ b/packages/base/src/msg/validatorset.rs @@ -1,10 +1,11 @@ -use crate::state::validatorset::{Config, ConfigOptional, ValidatorInfo}; +use crate::state::{ + provider_proposals::ProposalInfo, + validatorset::{ConfigOptional, ValidatorInfo}, +}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Decimal; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; -use crate::state::provider_proposals::ProposalInfo; - #[cw_serde] pub struct InstantiateMsg { pub owner: String, @@ -58,7 +59,7 @@ pub struct ValidatorResponse { #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - #[returns(Config)] + #[returns(crate::state::validatorset::Config)] Config {}, #[returns(ValidatorResponse)] Validator { valoper: String }, diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index c203ac5c..978ca1a4 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -28,6 +28,8 @@ pub struct Config { pub bond_limit: Option, pub fee: Option, pub fee_address: Option, + pub emergency_address: Option, + pub min_stake_amount: Uint128, } pub const CONFIG: Item = Item::new("config"); @@ -46,9 +48,10 @@ pub enum UnbondBatchStatus { UnbondRequested, UnbondFailed, Unbonding, - Unbonded, Withdrawing, Withdrawn, + WithdrawingEmergency, + WithdrawnEmergency, } #[cw_serde] @@ -138,6 +141,14 @@ const TRANSITIONS: &[Transition] = &[ from: ContractState::Unbonding, to: ContractState::Idle, }, + Transition { + from: ContractState::Transfering, + to: ContractState::Idle, + }, + Transition { + from: ContractState::Claiming, + to: ContractState::Idle, + }, ]; #[cw_serde] diff --git a/packages/puppeteer-base/src/msg.rs b/packages/puppeteer-base/src/msg.rs index 603c5898..c279597e 100644 --- a/packages/puppeteer-base/src/msg.rs +++ b/packages/puppeteer-base/src/msg.rs @@ -54,8 +54,9 @@ where } #[cw_serde] -pub struct TransferReadyBatchMsg { - pub batch_id: u128, +pub struct TransferReadyBatchesMsg { + pub batch_ids: Vec, + pub emergency: bool, pub amount: Uint128, pub recipient: String, } @@ -137,7 +138,7 @@ pub enum Transaction { interchain_account_id: String, validators: Vec, denom: String, - transfer: Option, + transfer: Option, }, IBCTransfer { denom: String, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 8142c301..7897a24d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.73.0" +channel = "1.75.0"