diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 402290360..8abf556cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev - uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: nightly-2023-12-21 profile: minimal override: true - name: Install cargo-tarpaulin @@ -46,7 +46,8 @@ jobs: "contracts/rewards-manager/*" --exclude-files "contracts/puppeteer-authz/*" --exclude-files "contracts/validators-stats/*" --exclude-files - "contracts/provider-proposals-poc/*" --exclude-files "*schema*" --out + "contracts/provider-proposals-poc/*" --exclude-files + "contracts/redemption-rate-adapter/*" --exclude-files "*schema*" --out Xml --output-dir ./ - name: Produce the coverage report uses: insightsengineering/coverage-action@v2 @@ -54,7 +55,7 @@ jobs: path: ./cobertura.xml threshold: 45 fail: true - publish: true + publish: false diff: false coverage-summary-title: Code Coverage Summary rustfmt: @@ -691,4 +692,4 @@ jobs: run: | docker stop -t0 $(docker ps -a -q) || true docker container prune -f || true - docker volume rm $(docker volume ls -q) || true + docker volume rm $(docker volume ls -q) || true \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7d12f83fd..b311b73c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,6 +1014,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-ownable", + "cw-utils 1.0.3", "cw2 1.1.2", "cw721 0.18.0", "drop-helpers", @@ -1023,6 +1024,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "drop-withdrawal-token" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-utils 1.0.3", + "cw2 1.1.2", + "drop-helpers", + "drop-staking-base", + "neutron-sdk", + "semver", + "thiserror", +] + [[package]] name = "drop-withdrawal-voucher" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 63fa3014d..72212685a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "contracts/factory", "contracts/hook-tester", "contracts/withdrawal-voucher", + "contracts/withdrawal-token", "contracts/withdrawal-manager", "contracts/proposal-votes-poc", "contracts/provider-proposals-poc", diff --git a/contracts/auto-withdrawer/src/contract.rs b/contracts/auto-withdrawer/src/contract.rs index a0d9904a8..612ed00af 100644 --- a/contracts/auto-withdrawer/src/contract.rs +++ b/contracts/auto-withdrawer/src/contract.rs @@ -7,22 +7,27 @@ use crate::{ store::{ bondings_map, reply::{CoreUnbond, CORE_UNBOND}, - BondingRecord, CORE_ADDRESS, LD_TOKEN, WITHDRAWAL_MANAGER_ADDRESS, - WITHDRAWAL_VOUCHER_ADDRESS, + BondingRecord, CORE_ADDRESS, LD_TOKEN, WITHDRAWAL_DENOM_PREFIX, WITHDRAWAL_MANAGER_ADDRESS, + WITHDRAWAL_TOKEN_ADDRESS, }, }; use cosmwasm_std::{ - attr, ensure, ensure_eq, to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, - Env, Event, MessageInfo, Order, Reply, Response, SubMsg, Uint64, WasmMsg, + attr, ensure, ensure_eq, ensure_ne, to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, + Decimal, Deps, DepsMut, Env, Event, MessageInfo, Order, Reply, Response, SubMsg, Uint128, + Uint64, WasmMsg, }; use cw_storage_plus::Bound; use drop_helpers::answer::response; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; +use std::collections::HashMap; +use std::fmt::Display; +use std::str::FromStr; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const CORE_UNBOND_REPLY_ID: u64 = 1; pub const PAGINATION_DEFAULT_LIMIT: Uint64 = Uint64::new(100u64); +pub const UNBOND_MARK: &str = "unbond"; #[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] pub fn instantiate( @@ -34,24 +39,26 @@ pub fn instantiate( cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; CORE_ADDRESS.save(deps.storage, &deps.api.addr_validate(&msg.core_address)?)?; - WITHDRAWAL_VOUCHER_ADDRESS.save( + WITHDRAWAL_TOKEN_ADDRESS.save( deps.storage, - &deps.api.addr_validate(&msg.withdrawal_voucher_address)?, + &deps.api.addr_validate(&msg.withdrawal_token_address)?, )?; WITHDRAWAL_MANAGER_ADDRESS.save( deps.storage, &deps.api.addr_validate(&msg.withdrawal_manager_address)?, )?; LD_TOKEN.save(deps.storage, &msg.ld_token)?; + WITHDRAWAL_DENOM_PREFIX.save(deps.storage, &msg.withdrawal_denom_prefix)?; Ok(response( "instantiate", CONTRACT_NAME, [ attr("core_address", msg.core_address), - attr("withdrawal_voucher", msg.withdrawal_voucher_address), + attr("withdrawal_token", msg.withdrawal_token_address), attr("withdrawal_manager", msg.withdrawal_manager_address), attr("ld_token", msg.ld_token), + attr("withdrawal_denom_prefix", msg.withdrawal_denom_prefix), ], )) } @@ -59,17 +66,23 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] pub fn execute( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> ContractResult> { match msg { ExecuteMsg::Bond(bond_msg) => match bond_msg { BondMsg::WithLdAssets {} => execute_bond_with_ld_assets(deps, info), - BondMsg::WithNFT { token_id } => execute_bond_with_nft(deps, env, info, token_id), + BondMsg::WithWithdrawalDenoms { batch_id } => { + execute_bond_with_withdrawal_denoms(deps, info, batch_id) + } }, - ExecuteMsg::Unbond { token_id } => execute_unbond(deps, info, token_id), - ExecuteMsg::Withdraw { token_id } => execute_withdraw(deps, info, token_id), + ExecuteMsg::Unbond { batch_id } => execute_unbond(deps, info, batch_id), + ExecuteMsg::Withdraw { + batch_id, + receiver, + amount, + } => execute_withdraw(deps, info, batch_id, receiver, amount), } } @@ -106,107 +119,134 @@ fn execute_bond_with_ld_assets( Ok(Response::new().add_submessage(SubMsg::reply_on_success(msg, CORE_UNBOND_REPLY_ID))) } -fn execute_bond_with_nft( +fn execute_bond_with_withdrawal_denoms( deps: DepsMut, - env: Env, - info: MessageInfo, - token_id: String, + mut info: MessageInfo, + batch_id: Uint128, ) -> ContractResult> { - let deposit = info.funds; + let withdrawal_denom_prefix = WITHDRAWAL_DENOM_PREFIX.load(deps.storage)?; + let withdrawal_token_address = WITHDRAWAL_TOKEN_ADDRESS.load(deps.storage)?; + let withdrawal_denom = + get_full_withdrawal_denom(withdrawal_denom_prefix, withdrawal_token_address, batch_id); + + let mut withdrawal_asset = info.funds.swap_remove( + info.funds + .iter() + .position(|coin| coin.denom == withdrawal_denom) + .ok_or(ContractError::WithdrawalAssetExpected {})?, + ); + let mut deposit = info.funds; ensure!(!deposit.is_empty(), ContractError::DepositExpected {}); // XXX: this code allows user to pass ld_token as a deposit. This sounds strange, but it might actually make // sense to do so. Should we introduce a check that forbids it? + let bonding_id = get_bonding_id(&info.sender, batch_id); + let existing_bonding = bondings_map().may_load(deps.storage, &bonding_id)?; + if let Some(existing_bonding) = existing_bonding { + deposit = merge_deposits(existing_bonding.deposit, deposit); + withdrawal_asset.amount += existing_bonding.withdrawal_amount; + } bondings_map().save( deps.storage, - &token_id, + &bonding_id, &BondingRecord { bonder: info.sender, + withdrawal_amount: withdrawal_asset.amount, deposit, }, )?; - let withdrawal_voucher = WITHDRAWAL_VOUCHER_ADDRESS.load(deps.storage)?; - let msg = WasmMsg::Execute { - contract_addr: withdrawal_voucher.into_string(), - msg: to_json_binary( - &drop_staking_base::msg::withdrawal_voucher::ExecuteMsg::TransferNft { - recipient: env.contract.address.into_string(), - token_id, - }, - )?, - funds: vec![], - }; - // TODO: attributes - Ok(Response::new().add_message(msg)) + Ok(Response::new()) } fn execute_unbond( deps: DepsMut, info: MessageInfo, - token_id: String, + batch_id: Uint128, ) -> ContractResult> { - let bonding = bondings_map().load(deps.storage, &token_id)?; + let bonding_id = get_bonding_id(&info.sender, batch_id); + let bonding = bondings_map().load(deps.storage, &bonding_id)?; ensure_eq!(info.sender, bonding.bonder, ContractError::Unauthorized {}); - bondings_map().remove(deps.storage, &token_id)?; - - let withdrawal_voucher = WITHDRAWAL_VOUCHER_ADDRESS.load(deps.storage)?; - - let nft_msg: CosmosMsg = WasmMsg::Execute { - contract_addr: withdrawal_voucher.into_string(), - msg: to_json_binary( - &drop_staking_base::msg::withdrawal_voucher::ExecuteMsg::TransferNft { - recipient: info.sender.to_string(), - token_id, - }, - )?, - funds: vec![], - } - .into(); - - let deposit_msg = BankMsg::Send { - to_address: info.sender.into_string(), - amount: bonding.deposit, - } - .into(); + bondings_map().remove(deps.storage, &bonding_id)?; + + let withdrawal_denom_prefix = WITHDRAWAL_DENOM_PREFIX.load(deps.storage)?; + let withdrawal_token_address = WITHDRAWAL_TOKEN_ADDRESS.load(deps.storage)?; + let withdrawal_denom = + get_full_withdrawal_denom(withdrawal_denom_prefix, withdrawal_token_address, batch_id); + + let mut send_assets_vec = vec![Coin::new( + bonding.withdrawal_amount.u128(), + withdrawal_denom, + )]; + send_assets_vec.extend(bonding.deposit); + + let send_assets_msg: BankMsg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: send_assets_vec, + }; // TODO: attributes - Ok(Response::new().add_messages([nft_msg, deposit_msg])) + Ok(Response::new().add_messages([send_assets_msg])) } fn execute_withdraw( deps: DepsMut, info: MessageInfo, - token_id: String, + batch_id: Uint128, + receiver: Option, + amount: Uint128, ) -> ContractResult> { - let bonding = bondings_map().load(deps.storage, &token_id)?; - bondings_map().remove(deps.storage, &token_id)?; + let bonder = receiver.unwrap_or(info.sender.clone()); + + let bonding_id = get_bonding_id(&bonder, batch_id); + let bonding = bondings_map().load(deps.storage, &bonding_id)?; + + ensure_ne!(amount, Uint128::zero(), ContractError::NothingToWithdraw {}); + ensure!( + amount <= bonding.withdrawal_amount, + ContractError::WithdrawnAmountTooBig {} + ); + + let sender_share = Decimal::from_ratio(amount, bonding.withdrawal_amount); + let divided_deposit = divide_deposits(bonding.deposit.clone(), sender_share); + + if amount < bonding.withdrawal_amount { + bondings_map().save( + deps.storage, + &bonding_id, + &BondingRecord { + bonder: bonder.clone(), + withdrawal_amount: bonding.withdrawal_amount - amount, + deposit: subtract_deposits(bonding.deposit.clone(), divided_deposit.clone()), + }, + )?; + } else { + bondings_map().remove(deps.storage, &bonding_id)?; + } + + let withdrawal_denom_prefix = WITHDRAWAL_DENOM_PREFIX.load(deps.storage)?; + let withdrawal_token_address = WITHDRAWAL_TOKEN_ADDRESS.load(deps.storage)?; + let withdrawal_denom = + get_full_withdrawal_denom(withdrawal_denom_prefix, withdrawal_token_address, batch_id); - let withdrawal_voucher = WITHDRAWAL_VOUCHER_ADDRESS.load(deps.storage)?; let withdrawal_manager = WITHDRAWAL_MANAGER_ADDRESS.load(deps.storage)?; let withdraw_msg: CosmosMsg = WasmMsg::Execute { - contract_addr: withdrawal_voucher.into_string(), + contract_addr: withdrawal_manager.into_string(), msg: to_json_binary( - &drop_staking_base::msg::withdrawal_voucher::ExecuteMsg::SendNft { - contract: withdrawal_manager.into_string(), - token_id, - msg: to_json_binary( - &drop_staking_base::msg::withdrawal_manager::ReceiveNftMsg::Withdraw { - receiver: Some(bonding.bonder.into_string()), - }, - )?, + &drop_staking_base::msg::withdrawal_manager::ExecuteMsg::ReceiveWithdrawalDenoms { + receiver: Some(bonder.into_string()), }, )?, - funds: vec![], + funds: vec![Coin::new(amount.u128(), withdrawal_denom)], } .into(); let deposit_msg = BankMsg::Send { - to_address: info.sender.into_string(), - amount: bonding.deposit, + to_address: info.sender.clone().into_string(), + amount: divided_deposit, } .into(); @@ -226,33 +266,123 @@ pub fn reply( CORE_UNBOND.remove(deps.storage); // it is safe to use unwrap() here since this reply is only called on success let events = reply.result.unwrap().events; + deps.api.debug(&format!("WASMDEBUG: {:?}", events)); reply_core_unbond(deps, sender, deposit, events) } - _ => unreachable!(), + id => Err(ContractError::InvalidCoreReplyId { id }), + } +} + +fn get_core_event_value(events: &[Event], key: &str) -> String { + events + .iter() + .filter(|event| event.ty == "wasm-drop-withdrawal-token-execute-mint") + .flat_map(|event| event.attributes.iter()) + .find(|attribute| attribute.key == key) + .unwrap() + .value + .clone() +} + +fn merge_deposits(vec1: Vec, vec2: Vec) -> Vec { + let mut coin_map: HashMap = HashMap::new(); + + for coin in vec1.into_iter().chain(vec2.into_iter()) { + coin_map + .entry(coin.denom) + .and_modify(|e| *e += coin.amount) + .or_insert(coin.amount); + } + + let mut merged_coins: Vec = coin_map + .into_iter() + .map(|(denom, amount)| Coin { denom, amount }) + .collect(); + + merged_coins.sort_by(|a, b| a.denom.cmp(&b.denom)); + + merged_coins +} + +fn divide_deposits(coins: Vec, multiplier: Decimal) -> Vec { + coins + .into_iter() + .map(|coin| Coin { + denom: coin.denom, + amount: coin.amount * multiplier, + }) + .collect() +} + +fn subtract_deposits(vec1: Vec, vec2: Vec) -> Vec { + let mut coin_map: HashMap = HashMap::new(); + + for coin in vec1 { + coin_map.insert(coin.denom.clone(), coin.amount); + } + + for coin in vec2 { + coin_map + .entry(coin.denom.clone()) + .and_modify(|e| { + if *e >= coin.amount { + *e -= coin.amount; + } + }) + .or_insert(Uint128::zero()); } + + let mut result_coins: Vec = coin_map + .into_iter() + .filter(|(_, amount)| !amount.is_zero()) + .map(|(denom, amount)| Coin { denom, amount }) + .collect(); + + result_coins.sort_by(|a, b| a.denom.cmp(&b.denom)); + + result_coins +} + +fn get_bonding_id(sender: impl Display, batch_id: impl Display) -> String { + format!("{sender}_{batch_id}") +} + +fn get_full_withdrawal_denom( + withdrawal_denom_prefix: impl Display, + withdrawal_token_address: impl Display, + batch_id: Uint128, +) -> String { + format!("factory/{withdrawal_token_address}/{withdrawal_denom_prefix}:{UNBOND_MARK}:{batch_id}") } fn reply_core_unbond( deps: DepsMut, sender: Addr, - deposit: Vec, + mut deposit: Vec, events: Vec, ) -> ContractResult> { - let token_id = events - .into_iter() - .filter(|event| event.ty == "wasm") - .flat_map(|event| event.attributes) - .find(|attribute| attribute.key == "token_id") - // it is safe to use unwrap here because cw-721 always generates valid events on success - .unwrap() - .value; + let batch_id = get_core_event_value(&events, "batch_id"); + let str_amount = get_core_event_value(&events, "amount"); + + let mut amount = match Uint128::from_str(&str_amount) { + Ok(value) => Ok(value), + Err(_) => Err(ContractError::InvalidCoreReplyAttributes {}), + }?; + let bonding_id = get_bonding_id(&sender, batch_id); + + let existing_bonding = bondings_map().may_load(deps.storage, &bonding_id)?; + if let Some(existing_bonding) = existing_bonding { + deposit = merge_deposits(existing_bonding.deposit, deposit); + amount += existing_bonding.withdrawal_amount; + } bondings_map().save( deps.storage, - &token_id, + &bonding_id, &BondingRecord { bonder: sender, deposit, + withdrawal_amount: amount, }, )?; @@ -298,18 +428,19 @@ fn query_all_bondings( let mut bondings = vec![]; for i in (&mut iter).take(usize_limit) { - let (token_id, bonding) = i?; + let (bonding_id, bonding) = i?; bondings.push(BondingResponse { - token_id, + bonding_id, bonder: bonding.bonder.into_string(), deposit: bonding.deposit, + withdrawal_amount: bonding.withdrawal_amount, }) } let next_page_key = iter .next() .transpose()? - .map(|(token_id, _bonding)| token_id); + .map(|(bonding_id, _bonding)| bonding_id); Ok(to_json_binary(&BondingsResponse { bondings, @@ -320,7 +451,8 @@ fn query_all_bondings( fn query_config(deps: Deps) -> ContractResult { Ok(to_json_binary(&InstantiateMsg { core_address: CORE_ADDRESS.load(deps.storage)?.into_string(), - withdrawal_voucher_address: WITHDRAWAL_VOUCHER_ADDRESS.load(deps.storage)?.into_string(), + withdrawal_token_address: WITHDRAWAL_TOKEN_ADDRESS.load(deps.storage)?.into_string(), + withdrawal_denom_prefix: WITHDRAWAL_DENOM_PREFIX.load(deps.storage)?, withdrawal_manager_address: WITHDRAWAL_MANAGER_ADDRESS.load(deps.storage)?.into_string(), ld_token: LD_TOKEN.load(deps.storage)?, })?) diff --git a/contracts/auto-withdrawer/src/error.rs b/contracts/auto-withdrawer/src/error.rs index e9a97e1d6..e1918dbb5 100644 --- a/contracts/auto-withdrawer/src/error.rs +++ b/contracts/auto-withdrawer/src/error.rs @@ -12,11 +12,26 @@ pub enum ContractError { #[error("no deposit was provided")] DepositExpected {}, + #[error("no withdrawal asset was provided")] + WithdrawalAssetExpected {}, + + #[error("withdrawn amount is too big")] + WithdrawnAmountTooBig {}, + + #[error("amount to withdraw is zero")] + NothingToWithdraw {}, + #[error("Semver parsing error: {0}")] SemVer(String), #[error("Bondings query limit exceeded")] QueryBondingsLimitExceeded {}, + + #[error("Core replies with invalid data")] + InvalidCoreReplyId { id: u64 }, + + #[error("Bondings query limit exceeded")] + InvalidCoreReplyAttributes {}, } impl From for ContractError { diff --git a/contracts/auto-withdrawer/src/msg.rs b/contracts/auto-withdrawer/src/msg.rs index c8d84c379..bfeb20846 100644 --- a/contracts/auto-withdrawer/src/msg.rs +++ b/contracts/auto-withdrawer/src/msg.rs @@ -1,25 +1,32 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, Uint64}; +use cosmwasm_std::{Addr, Coin, Uint128, Uint64}; #[cw_serde] pub struct InstantiateMsg { pub core_address: String, - pub withdrawal_voucher_address: String, + pub withdrawal_token_address: String, pub withdrawal_manager_address: String, pub ld_token: String, + pub withdrawal_denom_prefix: String, } #[cw_serde] pub enum ExecuteMsg { Bond(BondMsg), - Unbond { token_id: String }, - Withdraw { token_id: String }, + Unbond { + batch_id: Uint128, + }, + Withdraw { + batch_id: Uint128, + receiver: Option, + amount: Uint128, + }, } #[cw_serde] pub enum BondMsg { WithLdAssets {}, - WithNFT { token_id: String }, + WithWithdrawalDenoms { batch_id: Uint128 }, } #[cw_serde] @@ -47,9 +54,10 @@ pub struct BondingsResponse { #[cw_serde] pub struct BondingResponse { - pub token_id: String, + pub bonding_id: String, pub bonder: String, pub deposit: Vec, + pub withdrawal_amount: Uint128, } #[cw_serde] diff --git a/contracts/auto-withdrawer/src/store.rs b/contracts/auto-withdrawer/src/store.rs index 30066f670..7a977087d 100644 --- a/contracts/auto-withdrawer/src/store.rs +++ b/contracts/auto-withdrawer/src/store.rs @@ -1,11 +1,13 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::{Addr, Coin, Uint128}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex}; pub const CORE_ADDRESS: Item = Item::new("core"); +pub const WITHDRAWAL_TOKEN_ADDRESS: Item = Item::new("withdrawal_token"); pub const WITHDRAWAL_VOUCHER_ADDRESS: Item = Item::new("withdrawal_voucher"); pub const WITHDRAWAL_MANAGER_ADDRESS: Item = Item::new("withdrawal_manager"); pub const LD_TOKEN: Item = Item::new("ld_token"); +pub const WITHDRAWAL_DENOM_PREFIX: Item = Item::new("withdrawal_denom_prefix"); pub use bondings::{map as bondings_map, BondingRecord}; mod bondings { @@ -15,6 +17,7 @@ mod bondings { pub struct BondingRecord { pub bonder: Addr, pub deposit: Vec, + pub withdrawal_amount: Uint128, } pub struct BondingRecordIndexes<'a> { diff --git a/contracts/auto-withdrawer/src/tests.rs b/contracts/auto-withdrawer/src/tests.rs index 2bbd6dd17..56ccb16b7 100644 --- a/contracts/auto-withdrawer/src/tests.rs +++ b/contracts/auto-withdrawer/src/tests.rs @@ -1,13 +1,19 @@ +use crate::msg::{BondingsResponse, QueryMsg}; +use crate::store::reply::{CoreUnbond, CORE_UNBOND}; use crate::{ - contract, + contract::{self, CORE_UNBOND_REPLY_ID}, error::ContractError, msg::{BondMsg, ExecuteMsg, InstantiateMsg}, - store::{CORE_ADDRESS, LD_TOKEN, WITHDRAWAL_MANAGER_ADDRESS, WITHDRAWAL_VOUCHER_ADDRESS}, + store::{ + CORE_ADDRESS, LD_TOKEN, WITHDRAWAL_DENOM_PREFIX, WITHDRAWAL_MANAGER_ADDRESS, + WITHDRAWAL_TOKEN_ADDRESS, + }, }; use cosmwasm_std::{ - attr, coin, + attr, coin, from_json, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Event, OwnedDeps, Querier, + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Event, OwnedDeps, Querier, Reply, ReplyOn, + Response, SubMsg, SubMsgResponse, SubMsgResult, Uint128, Uint64, WasmMsg, }; use neutron_sdk::bindings::query::NeutronQuery; use std::marker::PhantomData; @@ -30,9 +36,10 @@ fn instantiate() { mock_info("admin", &[]), InstantiateMsg { core_address: "core".to_string(), - withdrawal_voucher_address: "withdrawal_voucher".to_string(), + withdrawal_token_address: "withdrawal_token".to_string(), withdrawal_manager_address: "withdrawal_manager".to_string(), ld_token: "ld_token".to_string(), + withdrawal_denom_prefix: "drop".to_string(), }, ) .unwrap(); @@ -41,14 +48,16 @@ fn instantiate() { assert_eq!(core, "core"); let ld_token = LD_TOKEN.load(deps.as_ref().storage).unwrap(); assert_eq!(ld_token, "ld_token"); - let withdrawal_voucher = WITHDRAWAL_VOUCHER_ADDRESS + let withdrawal_token = WITHDRAWAL_TOKEN_ADDRESS .load(deps.as_ref().storage) .unwrap(); - assert_eq!(withdrawal_voucher, "withdrawal_voucher"); + assert_eq!(withdrawal_token, "withdrawal_token"); let withdrawal_manager = WITHDRAWAL_MANAGER_ADDRESS .load(deps.as_ref().storage) .unwrap(); assert_eq!(withdrawal_manager, "withdrawal_manager"); + let withdrawal_denom_prefix = WITHDRAWAL_DENOM_PREFIX.load(deps.as_ref().storage).unwrap(); + assert_eq!(withdrawal_denom_prefix, "drop"); assert_eq!(response.messages.len(), 0); assert_eq!( @@ -56,9 +65,10 @@ fn instantiate() { vec![ Event::new("drop-auto-withdrawer-instantiate").add_attributes([ attr("core_address", "core"), - attr("withdrawal_voucher", "withdrawal_voucher"), + attr("withdrawal_token", "withdrawal_token"), attr("withdrawal_manager", "withdrawal_manager"), - attr("ld_token", "ld_token") + attr("ld_token", "ld_token"), + attr("withdrawal_denom_prefix", "drop") ]) ] ); @@ -82,6 +92,33 @@ fn bond_missing_ld_assets() { assert_eq!(err, ContractError::LdTokenExpected {}); } +#[test] +fn bond_missing_withdrawal_denoms() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let err = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::WithdrawalAssetExpected {}); +} + mod bond_missing_deposit { use super::*; @@ -105,12 +142,26 @@ mod bond_missing_deposit { #[test] fn with_nft() { let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + let err = contract::execute( deps.as_mut(), mock_env(), - mock_info("sender", &[]), - ExecuteMsg::Bond(BondMsg::WithNFT { - token_id: "token_id".into(), + mock_info( + "sender", + &[coin(10, "factory/withdrawal_token_contract/drop:unbond:0")], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), }), ) .unwrap_err(); @@ -118,3 +169,851 @@ mod bond_missing_deposit { assert_eq!(err, ContractError::DepositExpected {}); } } + +#[test] +fn bond_with_ld_assets_happy_path() { + let mut deps = mock_dependencies::(); + + LD_TOKEN + .save(deps.as_mut().storage, &"ld_token".into()) + .unwrap(); + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[coin(10, "ld_token"), coin(20, "untrn")]), + ExecuteMsg::Bond(BondMsg::WithLdAssets {}), + ) + .unwrap(); + + assert_eq!(response.messages.len(), 1); + assert_eq!(response.messages[0].reply_on, ReplyOn::Success); + assert_eq!(response.messages[0].id, CORE_UNBOND_REPLY_ID); + assert_eq!( + response.messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "core_contract".to_string(), + msg: to_json_binary(&drop_staking_base::msg::core::ExecuteMsg::Unbond {}).unwrap(), + funds: vec![Coin::new(10, "ld_token")], + }) + ); + assert!(response.events.is_empty()); + assert!(response.attributes.is_empty()); +} + +#[test] +fn reply_after_new_bond_with_ld_assets() { + let mut deps = drop_helpers::testing::mock_dependencies(&[]); + + CORE_UNBOND + .save( + deps.as_mut().storage, + &CoreUnbond { + sender: Addr::unchecked("sender"), + deposit: vec![Coin::new(10, "untrn")], + }, + ) + .unwrap(); + + let response = contract::reply( + deps.as_mut(), + mock_env(), + Reply { + id: CORE_UNBOND_REPLY_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![Event::new("wasm-drop-withdrawal-token-execute-mint") + .add_attribute("denom", "factory/withdrawal_token_contract/drop:unbond:0") + .add_attribute("receiver", "receiver") + .add_attribute("batch_id", "0") + .add_attribute("amount", "100")], + data: None, + }), + }, + ) + .unwrap(); + + assert_eq!(response, Response::new()); + + let res = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(res.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 1); + + assert_eq!(bondings_response.bondings[0].bonding_id, "sender_0"); + assert_eq!(bondings_response.bondings[0].bonder, "sender"); + + assert_eq!( + bondings_response.bondings[0].withdrawal_amount, + Uint128::from(100u64) + ); + + assert_eq!(bondings_response.bondings[0].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[0].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[0].deposit[0].amount, + Uint128::from(10u64) + ); +} + +#[test] +fn reply_after_existing_bond_with_ld_assets() { + let mut deps = drop_helpers::testing::mock_dependencies(&[]); + + CORE_UNBOND + .save( + deps.as_mut().storage, + &CoreUnbond { + sender: Addr::unchecked("sender"), + deposit: vec![Coin::new(8, "untrn")], + }, + ) + .unwrap(); + + let _ = contract::reply( + deps.as_mut(), + mock_env(), + Reply { + id: CORE_UNBOND_REPLY_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![Event::new("wasm-drop-withdrawal-token-execute-mint") + .add_attribute("denom", "factory/withdrawal_token_contract/drop:unbond:0") + .add_attribute("receiver", "receiver") + .add_attribute("batch_id", "0") + .add_attribute("amount", "80")], + data: None, + }), + }, + ); + + CORE_UNBOND + .save( + deps.as_mut().storage, + &CoreUnbond { + sender: Addr::unchecked("sender"), + deposit: vec![Coin::new(12, "untrn")], + }, + ) + .unwrap(); + + let response = contract::reply( + deps.as_mut(), + mock_env(), + Reply { + id: CORE_UNBOND_REPLY_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![Event::new("wasm-drop-withdrawal-token-execute-mint") + .add_attribute("denom", "factory/withdrawal_token_contract/drop:unbond:0") + .add_attribute("receiver", "receiver") + .add_attribute("batch_id", "0") + .add_attribute("amount", "120")], + data: None, + }), + }, + ) + .unwrap(); + + assert_eq!(response, Response::new()); + + let res = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(res.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 1); + + assert_eq!(bondings_response.bondings[0].bonding_id, "sender_0"); + assert_eq!(bondings_response.bondings[0].bonder, "sender"); + + assert_eq!( + bondings_response.bondings[0].withdrawal_amount, + Uint128::from(200u64) + ); + + assert_eq!(bondings_response.bondings[0].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[0].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[0].deposit[0].amount, + Uint128::from(20u64) + ); +} + +#[test] +fn reply_unknown_id() { + let mut deps = drop_helpers::testing::mock_dependencies(&[]); + let error = contract::reply( + deps.as_mut(), + mock_env(), + Reply { + id: 512, + result: SubMsgResult::Err("".to_string()), + }, + ) + .unwrap_err(); + assert_eq!(error, ContractError::InvalidCoreReplyId { id: 512 }); +} + +#[test] +fn reply_invalid_attribute() { + let mut deps = drop_helpers::testing::mock_dependencies(&[]); + + CORE_UNBOND + .save( + deps.as_mut().storage, + &CoreUnbond { + sender: Addr::unchecked("sender"), + deposit: vec![Coin::new(10, "untrn")], + }, + ) + .unwrap(); + + let error = contract::reply( + deps.as_mut(), + mock_env(), + Reply { + id: CORE_UNBOND_REPLY_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![Event::new("wasm-drop-withdrawal-token-execute-mint") + .add_attribute("denom", "factory/withdrawal_token_contract/drop:unbond:0") + .add_attribute("receiver", "receiver") + .add_attribute("batch_id", "0") + .add_attribute("amount", "invalid")], + data: None, + }), + }, + ) + .unwrap_err(); + + assert_eq!(error, ContractError::InvalidCoreReplyAttributes {}); +} + +#[test] +fn bond_with_withdrawal_denoms_for_new_bond() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "sender", + &[ + coin(100, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(10, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + assert_eq!(response, Response::new()); + + let res = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(res.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 1); + + assert_eq!(bondings_response.bondings[0].bonding_id, "sender_0"); + assert_eq!(bondings_response.bondings[0].bonder, "sender"); + + assert_eq!( + bondings_response.bondings[0].withdrawal_amount, + Uint128::from(100u64) + ); + + assert_eq!(bondings_response.bondings[0].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[0].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[0].deposit[0].amount, + Uint128::from(10u64) + ); +} + +#[test] +fn bond_with_withdrawal_denoms_for_existing_bond() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let _ = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "sender", + &[ + coin(120, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(12, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + let response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "sender", + &[ + coin(80, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(8, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + assert_eq!(response, Response::new()); + + let res = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(res.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 1); + + assert_eq!(bondings_response.bondings[0].bonding_id, "sender_0"); + assert_eq!(bondings_response.bondings[0].bonder, "sender"); + + assert_eq!( + bondings_response.bondings[0].withdrawal_amount, + Uint128::from(200u64) + ); + + assert_eq!(bondings_response.bondings[0].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[0].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[0].deposit[0].amount, + Uint128::from(20u64) + ); +} + +#[test] +fn second_bond_with_withdrawal_denoms() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let first_response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "first_sender", + &[ + coin(30, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(3, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + assert_eq!(first_response, Response::new()); + + let second_response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "second_sender", + &[ + coin(60, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(6, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + assert_eq!(second_response, Response::new()); + + let res = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(res.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 2); + + assert_eq!(bondings_response.bondings[0].bonding_id, "first_sender_0"); + assert_eq!(bondings_response.bondings[0].bonder, "first_sender"); + + assert_eq!(bondings_response.bondings[1].bonding_id, "second_sender_0"); + assert_eq!(bondings_response.bondings[1].bonder, "second_sender"); + + assert_eq!( + bondings_response.bondings[0].withdrawal_amount, + Uint128::from(30u64) + ); + assert_eq!( + bondings_response.bondings[1].withdrawal_amount, + Uint128::from(60u64) + ); + + assert_eq!(bondings_response.bondings[0].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[0].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[0].deposit[0].amount, + Uint128::from(3u64) + ); + + assert_eq!(bondings_response.bondings[1].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[1].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[1].deposit[0].amount, + Uint128::from(6u64) + ); +} + +#[test] +fn unbond_happy_path() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let _ = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "sender", + &[ + coin(100, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(10, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + let response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Unbond { + batch_id: Uint128::zero(), + }, + ) + .unwrap(); + + assert_eq!( + response, + Response::new().add_submessage(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "sender".to_string(), + amount: vec![ + Coin::new(100, "factory/withdrawal_token_contract/drop:unbond:0"), + Coin::new(10, "untrn") + ] + }))) + ); +} + +#[test] +fn execute_withdraw_nothing() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let _ = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "sender", + &[ + coin(100, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(10, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + let error = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Withdraw { + batch_id: Uint128::zero(), + receiver: None, + amount: Uint128::zero(), + }, + ) + .unwrap_err(); + + assert_eq!(error, ContractError::NothingToWithdraw {}); +} + +#[test] +fn execute_withdraw_too_much() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + + let _ = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "sender", + &[ + coin(100, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(10, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + let error = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Withdraw { + batch_id: Uint128::zero(), + receiver: None, + amount: Uint128::from(101u64), + }, + ) + .unwrap_err(); + + assert_eq!(error, ContractError::WithdrawnAmountTooBig {}); +} + +#[test] +fn execute_withdraw_full_amount() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + + let _ = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "bonder", + &[ + coin(100, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(10, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + let execute_response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Withdraw { + batch_id: Uint128::zero(), + receiver: Option::from(Addr::unchecked("bonder")), + amount: Uint128::from(100u64), + }, + ) + .unwrap(); + + assert_eq!( + execute_response, + Response::new() + .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_manager_contract".to_string(), + msg: to_json_binary(&drop_staking_base::msg::withdrawal_manager::ExecuteMsg::ReceiveWithdrawalDenoms { + receiver: Option::from("bonder".to_string()), + }) + .unwrap(), + funds: vec![Coin::new(100, "factory/withdrawal_token_contract/drop:unbond:0")], + }))) + .add_submessage(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "sender".to_string(), + amount: vec![Coin::new(10, "untrn")], + }))) + ); + + let query_response = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(query_response.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 0); +} + +#[test] +fn execute_withdraw_part_amount() { + let mut deps = mock_dependencies::(); + + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + + let _ = contract::execute( + deps.as_mut(), + mock_env(), + mock_info( + "bonder", + &[ + coin(100, "factory/withdrawal_token_contract/drop:unbond:0"), + coin(10, "untrn"), + ], + ), + ExecuteMsg::Bond(BondMsg::WithWithdrawalDenoms { + batch_id: Uint128::zero(), + }), + ) + .unwrap(); + + let execute_response = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Withdraw { + batch_id: Uint128::zero(), + receiver: Option::from(Addr::unchecked("bonder")), + amount: Uint128::from(70u64), + }, + ) + .unwrap(); + + assert_eq!( + execute_response, + Response::new() + .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_manager_contract".to_string(), + msg: to_json_binary(&drop_staking_base::msg::withdrawal_manager::ExecuteMsg::ReceiveWithdrawalDenoms { + receiver: Option::from("bonder".to_string()), + }) + .unwrap(), + funds: vec![Coin::new(70, "factory/withdrawal_token_contract/drop:unbond:0")], + }))) + .add_submessage(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "sender".to_string(), + amount: vec![Coin::new(7, "untrn")], + }))) + ); + + let query_response = contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::Bondings { + user: None, + limit: Option::from(Uint64::new(10u64)), + page_key: None, + }, + ); + + let bondings_response = from_json::(query_response.unwrap()).unwrap(); + assert_eq!(bondings_response.bondings.len(), 1); + assert_eq!(bondings_response.bondings[0].bonding_id, "bonder_0"); + assert_eq!(bondings_response.bondings[0].bonder, "bonder"); + assert_eq!( + bondings_response.bondings[0].withdrawal_amount, + Uint128::from(30u64) + ); + assert_eq!(bondings_response.bondings[0].deposit.len(), 1); + assert_eq!( + bondings_response.bondings[0].deposit[0].denom, + "untrn".to_string() + ); + assert_eq!( + bondings_response.bondings[0].deposit[0].amount, + Uint128::from(3u64) + ); +} + +#[test] +fn execute_query_config() { + let mut deps = mock_dependencies::(); + + LD_TOKEN + .save(deps.as_mut().storage, &"ld_token".into()) + .unwrap(); + WITHDRAWAL_DENOM_PREFIX + .save(deps.as_mut().storage, &"drop".into()) + .unwrap(); + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_TOKEN_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_token_contract"), + ) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + + let query_response = contract::query(deps.as_ref(), mock_env(), QueryMsg::Config {}); + + let config_response = from_json::(query_response.unwrap()).unwrap(); + assert_eq!(config_response.core_address, "core_contract"); + assert_eq!(config_response.ld_token, "ld_token"); + assert_eq!(config_response.withdrawal_denom_prefix, "drop"); + assert_eq!( + config_response.withdrawal_token_address, + "withdrawal_token_contract" + ); + assert_eq!( + config_response.withdrawal_manager_address, + "withdrawal_manager_contract" + ); +} diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index e6634bade..53c4ffc73 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -21,7 +21,7 @@ use drop_staking_base::{ ConfigResponse as TokenConfigResponse, ExecuteMsg as TokenExecuteMsg, QueryMsg as TokenQueryMsg, }, - withdrawal_voucher::ExecuteMsg as VoucherExecuteMsg, + withdrawal_token::ExecuteMsg as WithdrawalTokenExecuteMsg, }, state::{ core::{ @@ -32,7 +32,6 @@ use drop_staking_base::{ LD_DENOM, LSM_SHARES_TO_REDEEM, PENDING_LSM_SHARES, TOTAL_LSM_SHARES, UNBOND_BATCH_ID, }, validatorset::ValidatorInfo, - withdrawal_voucher::{Metadata, Trait}, }, }; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; @@ -73,9 +72,12 @@ pub fn instantiate( )? .denom, )?; - //an empty unbonding batch added as it's ready to be used on unbond action + + //an empty unbonding batch and first withdrawal denom are added as they`re ready to be used on unbond action UNBOND_BATCH_ID.save(deps.storage, &0)?; unbond_batches_map().save(deps.storage, 0, &new_unbond(env.block.time.seconds()))?; + let create_withdrawal_denom_result = send_create_withdrawal_denom_msg(Uint128::zero(), &config); + FSM.set_initial_state(deps.storage, ContractState::Idle)?; LAST_IDLE_CALL.save(deps.storage, &0)?; LAST_ICA_CHANGE_HEIGHT.save(deps.storage, &0)?; @@ -83,7 +85,7 @@ pub fn instantiate( BONDED_AMOUNT.save(deps.storage, &Uint128::zero())?; LAST_LSM_REDEEM.save(deps.storage, &env.block.time.seconds())?; BOND_HOOKS.save(deps.storage, &vec![])?; - Ok(response("instantiate", CONTRACT_NAME, attrs)) + Ok(response("instantiate", CONTRACT_NAME, attrs).add_message(create_withdrawal_denom_result)) } #[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] @@ -804,6 +806,14 @@ fn execute_tick_idle( FSM.go_to(deps.storage, ContractState::Unbonding)?; attrs.push(attr("knot", "029")); attrs.push(attr("state", "unbonding")); + + let batch_id = UNBOND_BATCH_ID.load(deps.storage)?; + let current_unbond_batch = unbond_batches_map().load(deps.storage, batch_id)?; + if current_unbond_batch.status == UnbondBatchStatus::New { + let create_withdrawal_denom_message = + send_create_withdrawal_denom_msg(Uint128::from(batch_id), config); + messages.push(create_withdrawal_denom_message); + } } else { attrs.push(attr("state", "idle")); attrs.push(attr("knot", "000")); @@ -943,6 +953,14 @@ fn execute_tick_claiming( FSM.go_to(deps.storage, ContractState::Unbonding)?; attrs.push(attr("knot", "029")); attrs.push(attr("state", "unbonding")); + + let batch_id = UNBOND_BATCH_ID.load(deps.storage)?; + let current_unbond_batch = unbond_batches_map().load(deps.storage, batch_id)?; + if current_unbond_batch.status == UnbondBatchStatus::New { + let create_withdrawal_denom_message = + send_create_withdrawal_denom_msg(Uint128::from(batch_id), config); + messages.push(create_withdrawal_denom_message); + } } else { FSM.go_to(deps.storage, ContractState::Idle)?; attrs.push(attr("knot", "000")); @@ -985,6 +1003,14 @@ fn execute_tick_staking_bond( FSM.go_to(deps.storage, ContractState::Unbonding)?; attrs.push(attr("knot", "029")); attrs.push(attr("state", "unbonding")); + + let batch_id = UNBOND_BATCH_ID.load(deps.storage)?; + let current_unbond_batch = unbond_batches_map().load(deps.storage, batch_id)?; + if current_unbond_batch.status == UnbondBatchStatus::New { + let create_withdrawal_denom_message = + send_create_withdrawal_denom_msg(Uint128::from(batch_id), config); + messages.push(create_withdrawal_denom_message); + } } else { FSM.go_to(deps.storage, ContractState::Idle)?; attrs.push(attr("knot", "000")); @@ -1159,6 +1185,10 @@ fn execute_update_config( config.staker_contract = deps.api.addr_validate(&staker_contract)?; attrs.push(attr("staker_contract", staker_contract)); } + if let Some(withdrawal_token_contract) = new_config.withdrawal_token_contract { + config.withdrawal_token_contract = deps.api.addr_validate(&withdrawal_token_contract)?; + attrs.push(attr("withdrawal_token_contract", withdrawal_token_contract)); + } if let Some(withdrawal_voucher_contract) = new_config.withdrawal_voucher_contract { config.withdrawal_voucher_contract = deps.api.addr_validate(&withdrawal_voucher_contract)?; @@ -1274,37 +1304,13 @@ fn execute_unbond( unbond_batch.total_dasset_amount_to_withdraw += dasset_amount; unbond_batches_map().save(deps.storage, unbond_batch_id, &unbond_batch)?; - let extension = Some(Metadata { - description: Some("Withdrawal voucher".into()), - name: "LDV voucher".to_string(), - batch_id: unbond_batch_id.to_string(), - amount: dasset_amount, - attributes: Some(vec![ - Trait { - display_type: None, - trait_type: "unbond_batch_id".to_string(), - value: unbond_batch_id.to_string(), - }, - Trait { - display_type: None, - trait_type: "received_amount".to_string(), - value: dasset_amount.to_string(), - }, - ]), - }); - let msgs = vec![ CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.withdrawal_voucher_contract.into_string(), - msg: to_json_binary(&VoucherExecuteMsg::Mint { - owner: info.sender.to_string(), - token_id: unbond_batch_id.to_string() - + "_" - + info.sender.to_string().as_str() - + "_" - + &unbond_batch.total_unbond_items.to_string(), - token_uri: None, - extension, + contract_addr: config.withdrawal_token_contract.into_string(), + msg: to_json_binary(&WithdrawalTokenExecuteMsg::Mint { + receiver: info.sender.to_string(), + amount: dasset_amount, + batch_id: Uint128::from(unbond_batch_id), })?, funds: vec![], }), @@ -1668,6 +1674,14 @@ fn calc_lsm_share_underlying_amount( )?) } +fn send_create_withdrawal_denom_msg(batch_id: Uint128, config: &Config) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.withdrawal_token_contract.to_string(), + msg: to_json_binary(&WithdrawalTokenExecuteMsg::CreateDenom { batch_id }).unwrap(), + funds: vec![], + }) +} + pub mod check_denom { use super::*; diff --git a/contracts/core/src/tests.rs b/contracts/core/src/tests.rs index 3dac17858..b66c9e674 100644 --- a/contracts/core/src/tests.rs +++ b/contracts/core/src/tests.rs @@ -51,6 +51,7 @@ fn get_default_config( token_contract: Addr::unchecked("token_contract"), puppeteer_contract: Addr::unchecked(MOCK_PUPPETEER_CONTRACT_ADDR), strategy_contract: Addr::unchecked(MOCK_STRATEGY_CONTRACT_ADDR), + withdrawal_token_contract: Addr::unchecked("withdrawal_token_contract"), withdrawal_voucher_contract: Addr::unchecked("withdrawal_voucher_contract"), withdrawal_manager_contract: Addr::unchecked("withdrawal_manager_contract"), validators_set_contract: Addr::unchecked("validators_set_contract"), @@ -109,6 +110,7 @@ fn test_update_config() { puppeteer_contract: "old_puppeteer_contract".to_string(), strategy_contract: "old_strategy_contract".to_string(), staker_contract: "old_staker_contract".to_string(), + withdrawal_token_contract: "old_withdrawal_token_contract".to_string(), withdrawal_voucher_contract: "old_withdrawal_voucher_contract".to_string(), withdrawal_manager_contract: "old_withdrawal_manager_contract".to_string(), validators_set_contract: "old_validators_set_contract".to_string(), @@ -141,6 +143,7 @@ fn test_update_config() { puppeteer_contract: Some("new_puppeteer_contract".to_string()), strategy_contract: Some("new_strategy_contract".to_string()), staker_contract: Some("new_staker_contract".to_string()), + withdrawal_token_contract: Some("new_withdrawal_token_contract".to_string()), withdrawal_voucher_contract: Some("new_withdrawal_voucher_contract".to_string()), withdrawal_manager_contract: Some("new_withdrawal_manager_contract".to_string()), validators_set_contract: Some("new_validators_set_contract".to_string()), @@ -165,6 +168,7 @@ fn test_update_config() { puppeteer_contract: Addr::unchecked("new_puppeteer_contract"), staker_contract: Addr::unchecked("new_staker_contract"), strategy_contract: Addr::unchecked("new_strategy_contract"), + withdrawal_token_contract: Addr::unchecked("new_withdrawal_token_contract"), withdrawal_voucher_contract: Addr::unchecked("new_withdrawal_voucher_contract"), withdrawal_manager_contract: Addr::unchecked("new_withdrawal_manager_contract"), validators_set_contract: Addr::unchecked("new_validators_set_contract"), @@ -258,11 +262,11 @@ fn test_update_withdrawn_amount() { }; unbond_batches_map() - .save(deps.as_mut().storage, 1, withdrawn_batch) + .save(deps.as_mut().storage, 0, withdrawn_batch) .unwrap(); unbond_batches_map() - .save(deps.as_mut().storage, 0, unbonding_batch) + .save(deps.as_mut().storage, 1, unbonding_batch) .unwrap(); let withdrawn_res = execute( @@ -270,14 +274,14 @@ fn test_update_withdrawn_amount() { mock_env().clone(), mock_info("withdrawal_manager_contract", &[]), ExecuteMsg::UpdateWithdrawnAmount { - batch_id: 1, + batch_id: 0, withdrawn_amount: Uint128::from(1001u128), }, ); assert!(withdrawn_res.is_ok()); let new_withdrawn_amount = unbond_batches_map() - .load(deps.as_mut().storage, 1) + .load(deps.as_mut().storage, 0) .unwrap() .withdrawn_amount; assert_eq!(new_withdrawn_amount, Some(Uint128::from(1001u128))); @@ -287,7 +291,7 @@ fn test_update_withdrawn_amount() { mock_env().clone(), mock_info("withdrawal_manager_contract", &[]), ExecuteMsg::UpdateWithdrawnAmount { - batch_id: 0, + batch_id: 1, withdrawn_amount: Uint128::from(2002u128), }, ) @@ -1355,6 +1359,16 @@ fn test_tick_idle_unbonding() { .unwrap(), funds: vec![Coin::new(1000, "untrn")], }))) + .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_token_contract".to_string(), + msg: to_json_binary( + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::CreateDenom { + batch_id: Uint128::one(), + } + ) + .unwrap(), + funds: vec![], + }))) ); } @@ -1581,6 +1595,16 @@ fn test_tick_idle_unbonding_failed() { .unwrap(), funds: vec![Coin::new(1000, "untrn")], }))) + .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_token_contract".to_string(), + msg: to_json_binary( + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::CreateDenom { + batch_id: Uint128::one(), + } + ) + .unwrap(), + funds: vec![], + }))) ); let current_batch_id = UNBOND_BATCH_ID.load(deps.as_ref().storage).unwrap(); assert_eq!(current_batch_id, 1); @@ -2067,6 +2091,16 @@ fn test_tick_claiming_wo_transfer_unbonding() { .unwrap(), funds: vec![Coin::new(1000u128, "untrn")], }))) + .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_token_contract".to_string(), + msg: to_json_binary( + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::CreateDenom { + batch_id: Uint128::one(), + } + ) + .unwrap(), + funds: vec![], + }))) ); let new_batch_id = UNBOND_BATCH_ID.load(deps.as_mut().storage).unwrap(); assert_eq!(new_batch_id, 1u128); @@ -2592,6 +2626,16 @@ fn test_tick_staking_to_unbonding() { .unwrap(), funds: vec![Coin::new(1000u128, "untrn")], }))) + .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_token_contract".to_string(), + msg: to_json_binary( + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::CreateDenom { + batch_id: Uint128::one(), + } + ) + .unwrap(), + funds: vec![], + }))) ); } @@ -3592,35 +3636,16 @@ fn test_unbond() { ) .unwrap(); let unbond_batch = unbond_batches_map().load(deps.as_ref().storage, 0).unwrap(); - let extension = Some(drop_staking_base::state::withdrawal_voucher::Metadata { - description: Some("Withdrawal voucher".into()), - name: "LDV voucher".to_string(), - batch_id: "0".to_string(), - amount: Uint128::from(1000u128), - attributes: Some(vec![ - drop_staking_base::state::withdrawal_voucher::Trait { - display_type: None, - trait_type: "unbond_batch_id".to_string(), - value: "0".to_string(), - }, - drop_staking_base::state::withdrawal_voucher::Trait { - display_type: None, - trait_type: "received_amount".to_string(), - value: "1000".to_string(), - }, - ]), - }); assert_eq!( res, Response::new() .add_submessage(SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "withdrawal_voucher_contract".to_string(), + contract_addr: "withdrawal_token_contract".to_string(), msg: to_json_binary( - &drop_staking_base::msg::withdrawal_voucher::ExecuteMsg::Mint { - token_id: "0_some_sender_1".to_string(), - owner: "some_sender".to_string(), - token_uri: None, - extension, + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::Mint { + receiver: "some_sender".to_string(), + amount: Uint128::from(1000u128), + batch_id: Uint128::zero(), } ) .unwrap(), diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs index f2da340e5..ab79c3369 100644 --- a/contracts/factory/src/contract.rs +++ b/contracts/factory/src/contract.rs @@ -30,6 +30,7 @@ use drop_staking_base::{ InstantiateMsg as WithdrawalManagerInstantiateMsg, QueryMsg as WithdrawalManagerQueryMsg, }, + withdrawal_token::InstantiateMsg as WithdrawalTokenInstantiateMsg, withdrawal_voucher::InstantiateMsg as WithdrawalVoucherInstantiateMsg, }, state::pump::PumpTimeout, @@ -67,6 +68,8 @@ pub fn instantiate( let canonical_self_address = deps.api.addr_canonicalize(env.contract.address.as_str())?; let token_contract_checksum = get_code_checksum(deps.as_ref(), msg.code_ids.token_code_id)?; let core_contract_checksum = get_code_checksum(deps.as_ref(), msg.code_ids.core_code_id)?; + let withdrawal_token_contract_checksum = + get_code_checksum(deps.as_ref(), msg.code_ids.withdrawal_token_code_id)?; let withdrawal_voucher_contract_checksum = get_code_checksum(deps.as_ref(), msg.code_ids.withdrawal_voucher_code_id)?; let withdrawal_manager_contract_checksum = @@ -101,6 +104,16 @@ pub fn instantiate( instantiate2_address(&staker_contract_checksum, &canonical_self_address, salt)?; attrs.push(attr("staker_address", staker_address.to_string())); + let withdrawal_token_address = instantiate2_address( + &withdrawal_token_contract_checksum, + &canonical_self_address, + salt, + )?; + attrs.push(attr( + "withdrawal_token_address", + withdrawal_token_address.to_string(), + )); + let withdrawal_voucher_address = instantiate2_address( &withdrawal_voucher_contract_checksum, &canonical_self_address, @@ -170,6 +183,10 @@ pub fn instantiate( )); let core_contract = deps.api.addr_humanize(&core_address)?.to_string(); let token_contract = deps.api.addr_humanize(&token_address)?.to_string(); + let withdrawal_token_contract = deps + .api + .addr_humanize(&withdrawal_token_address)? + .to_string(); let withdrawal_voucher_contract = deps .api .addr_humanize(&withdrawal_voucher_address)? @@ -197,6 +214,7 @@ pub fn instantiate( core_contract: core_contract.to_string(), puppeteer_contract: puppeteer_contract.to_string(), staker_contract: staker_contract.to_string(), + withdrawal_token_contract: withdrawal_token_contract.to_string(), withdrawal_voucher_contract: withdrawal_voucher_contract.to_string(), withdrawal_manager_contract: withdrawal_manager_contract.to_string(), strategy_contract: strategy_contract.to_string(), @@ -215,7 +233,7 @@ pub fn instantiate( label: get_contract_label("token"), msg: to_json_binary(&TokenInstantiateMsg { core_address: core_contract.to_string(), - subdenom: msg.subdenom, + subdenom: msg.subdenom.clone(), token_metadata: msg.token_metadata, owner: env.contract.address.to_string(), })?, @@ -293,6 +311,19 @@ pub fn instantiate( funds: vec![], salt: Binary::from(salt), }), + CosmosMsg::Wasm(WasmMsg::Instantiate2 { + admin: Some(env.contract.address.to_string()), + code_id: msg.code_ids.withdrawal_token_code_id, + label: get_contract_label("withdrawal-token"), + msg: to_json_binary(&WithdrawalTokenInstantiateMsg { + core_address: core_contract.to_string(), + withdrawal_manager_address: withdrawal_manager_contract.to_string(), + denom_prefix: msg.subdenom.clone(), + owner: env.contract.address.to_string(), + })?, + funds: vec![], + salt: Binary::from(salt), + }), CosmosMsg::Wasm(WasmMsg::Instantiate2 { admin: Some(env.contract.address.to_string()), code_id: msg.code_ids.core_code_id, @@ -302,6 +333,7 @@ pub fn instantiate( puppeteer_contract: puppeteer_contract.to_string(), staker_contract: staker_contract.to_string(), strategy_contract: strategy_contract.to_string(), + withdrawal_token_contract: withdrawal_token_contract.to_string(), withdrawal_voucher_contract: withdrawal_voucher_contract.to_string(), withdrawal_manager_contract: withdrawal_manager_contract.to_string(), base_denom: msg.base_denom.clone(), @@ -343,6 +375,7 @@ pub fn instantiate( label: get_contract_label("withdrawal-manager"), msg: to_json_binary(&WithdrawalManagerInstantiateMsg { core_contract: core_contract.to_string(), + token_contract: withdrawal_token_contract.to_string(), voucher_contract: withdrawal_voucher_contract.to_string(), owner: env.contract.address.to_string(), base_denom: msg.base_denom.to_string(), diff --git a/contracts/factory/src/state.rs b/contracts/factory/src/state.rs index 9fa8994ad..8ea767131 100644 --- a/contracts/factory/src/state.rs +++ b/contracts/factory/src/state.rs @@ -7,6 +7,7 @@ pub struct CodeIds { pub core_code_id: u64, pub puppeteer_code_id: u64, pub staker_code_id: u64, + pub withdrawal_token_code_id: u64, pub withdrawal_voucher_code_id: u64, pub withdrawal_manager_code_id: u64, pub strategy_code_id: u64, @@ -40,6 +41,7 @@ pub struct State { pub core_contract: String, pub puppeteer_contract: String, pub staker_contract: String, + pub withdrawal_token_contract: String, pub withdrawal_voucher_contract: String, pub withdrawal_manager_contract: String, pub strategy_contract: String, diff --git a/contracts/factory/src/tests.rs b/contracts/factory/src/tests.rs index 14b062d98..91ad56f13 100644 --- a/contracts/factory/src/tests.rs +++ b/contracts/factory/src/tests.rs @@ -32,6 +32,7 @@ use drop_staking_base::{ ExecuteMsg as WithdrawalManagerExecuteMsg, InstantiateMsg as WithdrawalManagerInstantiateMsg, }, + withdrawal_token::InstantiateMsg as WithdrawalTokenInstantiateMsg, withdrawal_voucher::InstantiateMsg as WithdrawalVoucherInstantiateMsg, }, state::{pump::PumpTimeout, splitter::Config as SplitterConfig}, @@ -43,6 +44,7 @@ fn get_default_factory_state() -> State { core_contract: "core_contract".to_string(), puppeteer_contract: "puppeteer_contract".to_string(), staker_contract: "staker_contract".to_string(), + withdrawal_token_contract: "withdrawal_token_contract".to_string(), withdrawal_voucher_contract: "withdrawal_voucher_contract".to_string(), withdrawal_manager_contract: "withdrawal_manager_contract".to_string(), strategy_contract: "strategy_contract".to_string(), @@ -76,14 +78,15 @@ fn test_instantiate() { core_code_id: 2, puppeteer_code_id: 3, staker_code_id: 4, - withdrawal_voucher_code_id: 5, - withdrawal_manager_code_id: 6, - strategy_code_id: 7, - validators_set_code_id: 8, - distribution_code_id: 9, - rewards_manager_code_id: 10, - rewards_pump_code_id: 11, - splitter_code_id: 12, + withdrawal_token_code_id: 5, + withdrawal_voucher_code_id: 6, + withdrawal_manager_code_id: 7, + strategy_code_id: 8, + validators_set_code_id: 9, + distribution_code_id: 10, + rewards_manager_code_id: 11, + rewards_pump_code_id: 12, + splitter_code_id: 13, }, remote_opts: RemoteOpts { denom: "denom".to_string(), @@ -180,7 +183,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 8, + code_id: 9, label: "validators set".to_string(), msg: to_json_binary(&ValidatorsSetInstantiateMsg { owner: "factory_contract".to_string(), @@ -195,7 +198,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 9, + code_id: 10, label: "distribution".to_string(), msg: to_json_binary(&DistributionInstantiateMsg {}).unwrap(), funds: vec![], @@ -252,7 +255,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 7, + code_id: 8, label: "strategy".to_string(), msg: to_json_binary(&StrategyInstantiateMsg { owner: "factory_contract".to_string(), @@ -266,6 +269,22 @@ fn test_instantiate() { salt: cosmwasm_std::Binary::from("salt".as_bytes()) } )), + cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( + cosmwasm_std::WasmMsg::Instantiate2 { + admin: Some("factory_contract".to_string()), + code_id: 5, + label: "drop-staking-withdrawal-token".to_string(), + msg: to_json_binary(&WithdrawalTokenInstantiateMsg { + core_address: "some_humanized_address".to_string(), + withdrawal_manager_address: "some_humanized_address".to_string(), + denom_prefix: "subdenom".to_string(), + owner: "factory_contract".to_string(), + }) + .unwrap(), + funds: vec![], + salt: cosmwasm_std::Binary::from("salt".as_bytes()) + } + )), cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), @@ -276,6 +295,7 @@ fn test_instantiate() { puppeteer_contract: "some_humanized_address".to_string(), strategy_contract: "some_humanized_address".to_string(), staker_contract: "some_humanized_address".to_string(), + withdrawal_token_contract: "some_humanized_address".to_string(), withdrawal_voucher_contract: "some_humanized_address".to_string(), withdrawal_manager_contract: "some_humanized_address".to_string(), validators_set_contract: "some_humanized_address".to_string(), @@ -304,7 +324,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 5, + code_id: 6, label: "drop-staking-withdrawal-voucher".to_string(), msg: to_json_binary(&WithdrawalVoucherInstantiateMsg { name: "Drop Voucher".to_string(), @@ -319,10 +339,11 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 6, + code_id: 7, label: "drop-staking-withdrawal-manager".to_string(), msg: to_json_binary(&WithdrawalManagerInstantiateMsg { core_contract: "some_humanized_address".to_string(), + token_contract: "some_humanized_address".to_string(), voucher_contract: "some_humanized_address".to_string(), base_denom: "base_denom".to_string(), owner: "factory_contract".to_string() @@ -335,7 +356,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 10, + code_id: 11, label: "drop-staking-rewards-manager".to_string(), msg: to_json_binary(&RewardsManagerInstantiateMsg { owner: "factory_contract".to_string() @@ -348,7 +369,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 12, + code_id: 13, label: "drop-staking-splitter".to_string(), msg: to_json_binary(&SplitterInstantiateMsg { config: SplitterConfig { @@ -370,7 +391,7 @@ fn test_instantiate() { cosmwasm_std::SubMsg::new(cosmwasm_std::CosmosMsg::Wasm( cosmwasm_std::WasmMsg::Instantiate2 { admin: Some("factory_contract".to_string()), - code_id: 11, + code_id: 12, label: "drop-staking-rewards-pump".to_string(), msg: to_json_binary(&RewardsPumpInstantiateMsg { dest_address: Some("some_humanized_address".to_string()), @@ -407,14 +428,15 @@ fn test_instantiate() { core_code_id: 2, puppeteer_code_id: 3, staker_code_id: 4, - withdrawal_voucher_code_id: 5, - withdrawal_manager_code_id: 6, - strategy_code_id: 7, - validators_set_code_id: 8, - distribution_code_id: 9, - rewards_manager_code_id: 10, - rewards_pump_code_id: 11, - splitter_code_id: 12, + withdrawal_token_code_id: 5, + withdrawal_voucher_code_id: 6, + withdrawal_manager_code_id: 7, + strategy_code_id: 8, + validators_set_code_id: 9, + distribution_code_id: 10, + rewards_manager_code_id: 11, + rewards_pump_code_id: 12, + splitter_code_id: 13, } ) ), @@ -455,36 +477,40 @@ fn test_instantiate() { "186C6D9A24AD6C14B6CDC4E8636FCC3564F17691116D96A533C0D3E5B8EB7099" ), cosmwasm_std::attr( - "withdrawal_voucher_address", + "withdrawal_token_address", "2EDC9F3CA74FEE8C9C97C5804C5E44B684380772A0C7CFC7FEB14843D437AADD" ), cosmwasm_std::attr( - "withdrawal_manager_address", + "withdrawal_voucher_address", "9080C5A07DA3FAE670586720247E99E9CAB3DF2C79EBD967BEB03747252B97A0" ), cosmwasm_std::attr( - "strategy_address", + "withdrawal_manager_address", "A611AB41BFB88C7F467A747DFE64B3FE6141D563131D296421B5D4E038C84B9D" ), cosmwasm_std::attr( - "validators_set_address", + "strategy_address", "7B4BE8580FC3E8C59FB57516384CB9E30D3707E560C11C4818D94FE2707A8A6D" ), cosmwasm_std::attr( - "distribution_address", + "validators_set_address", "0368B2CE05A215FF095D4B598371205F6CDEAEE8A6332D233E0C6E7F099893AF" ), cosmwasm_std::attr( - "rewards_manager_address", + "distribution_address", "F71DD2183C67E30D7E199C9AB928419B8693183A24C2528790760290895FF118" ), + cosmwasm_std::attr( + "rewards_manager_address", + "6FC09E9FDF411D71139AB50762FB9862D4F519DC21EADB93E93195388E164F25" + ), cosmwasm_std::attr( "splitter_address", - "99F8E05523E1A4B79E291E7934CC8E306F609D5B470D9D0872925EDCA019D3C1" + "1D482178B63387218844AA22FC89A3D4465BD4CC07A8DC1232C62EFE4838F955" ), cosmwasm_std::attr( "rewards_pump_address", - "6FC09E9FDF411D71139AB50762FB9862D4F519DC21EADB93E93195388E164F25" + "99F8E05523E1A4B79E291E7934CC8E306F609D5B470D9D0872925EDCA019D3C1" ), ]) ) @@ -506,6 +532,7 @@ fn test_update_config_core_unauthorized() { puppeteer_contract: None, strategy_contract: None, staker_contract: None, + withdrawal_token_contract: None, withdrawal_voucher_contract: None, withdrawal_manager_contract: None, validators_set_contract: None, @@ -554,6 +581,7 @@ fn test_update_config_core() { puppeteer_contract: Some("puppeteer_contract1".to_string()), strategy_contract: Some("strategy_contract1".to_string()), staker_contract: Some("staker_contract1".to_string()), + withdrawal_token_contract: Some("withdrawal_token_contract1".to_string()), withdrawal_voucher_contract: Some("withdrawal_voucher_contract1".to_string()), withdrawal_manager_contract: Some("withdrawal_manager_contract1".to_string()), validators_set_contract: Some("validators_set_contract1".to_string()), diff --git a/contracts/withdrawal-exchange/.cargo/config b/contracts/withdrawal-exchange/.cargo/config new file mode 100644 index 000000000..a5e15b9d8 --- /dev/null +++ b/contracts/withdrawal-exchange/.cargo/config @@ -0,0 +1,2 @@ +[alias] +schema = "run --bin drop-withdrawal-exchange-schema" \ No newline at end of file diff --git a/contracts/withdrawal-exchange/Cargo.toml b/contracts/withdrawal-exchange/Cargo.toml new file mode 100644 index 000000000..27289097a --- /dev/null +++ b/contracts/withdrawal-exchange/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = ["Denis Zagumennov "] +description = "Contract which exchanges withdrawal NFTs to withdrawal denoms" +edition = "2021" +name = "drop-withdrawal-exchange" +version = "1.0.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +drop-staking-base = { workspace = true } +drop-helpers = { workspace = true } +cosmwasm-schema = { workspace = true } +cw2 = { workspace = true } +cw-utils = { workspace = true } +neutron-sdk = { workspace = true } +thiserror = { workspace = true } +cw-ownable = { workspace = true } +cosmos-sdk-proto = { workspace = true } +semver = { workspace = true } diff --git a/contracts/withdrawal-exchange/README.md b/contracts/withdrawal-exchange/README.md new file mode 100644 index 000000000..d9c1be7cb --- /dev/null +++ b/contracts/withdrawal-exchange/README.md @@ -0,0 +1,2 @@ +# DROP Withdrawal Exchange +It exchanges withdrawal NFTs to withdrawal denoms \ No newline at end of file diff --git a/contracts/withdrawal-exchange/src/contract.rs b/contracts/withdrawal-exchange/src/contract.rs new file mode 100644 index 000000000..cb92de63f --- /dev/null +++ b/contracts/withdrawal-exchange/src/contract.rs @@ -0,0 +1,57 @@ +use cosmwasm_std::{DepsMut, MessageInfo}; +use drop_staking_base::{ + error::withdrawal_exchange::{ContractError, ContractResult}, + msg::withdrawal_exchange::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + state::withdrawal_exchange::{WITHDRAWAL_TOKEN_ADDRESS}, +}; +use neutron_sdk::{bindings::{query::NeutronQuery}}; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult> { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.owner))?; + + let withdrawal_token = deps.api.addr_validate(&msg.withdrawal_token_address)?; + WITHDRAWAL_TOKEN_ADDRESS.save(deps.storage, &withdrawal_token)?; + + Ok(response( + "instantiate", + CONTRACT_NAME, + [ + attr("withdrawal_token_address", withdrawal_token), + ], + )) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::Config {} => { + let withdrawal_token_address = + WITHDRAWAL_TOKEN_ADDRESS.load(deps.storage)?.into_string(); + Ok(to_json_binary(&ConfigResponse { + withdrawal_token_address, + })?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult> { + match msg { + ExecuteMsg::Exchange { } => "bla", + } +} \ No newline at end of file diff --git a/contracts/withdrawal-exchange/src/lib.rs b/contracts/withdrawal-exchange/src/lib.rs new file mode 100644 index 000000000..b5eb8421b --- /dev/null +++ b/contracts/withdrawal-exchange/src/lib.rs @@ -0,0 +1 @@ +pub mod contract; \ No newline at end of file diff --git a/contracts/withdrawal-manager/Cargo.toml b/contracts/withdrawal-manager/Cargo.toml index a54f50f6f..6cec4d6e2 100644 --- a/contracts/withdrawal-manager/Cargo.toml +++ b/contracts/withdrawal-manager/Cargo.toml @@ -33,3 +33,4 @@ drop-staking-base = { workspace = true } drop-helpers = { workspace = true } thiserror = { workspace = true } semver = { workspace = true } +cw-utils = { workspace = true } diff --git a/contracts/withdrawal-manager/src/contract.rs b/contracts/withdrawal-manager/src/contract.rs index edaa621c5..00a99375c 100644 --- a/contracts/withdrawal-manager/src/contract.rs +++ b/contracts/withdrawal-manager/src/contract.rs @@ -44,6 +44,7 @@ pub fn instantiate( deps.storage, &Config { core_contract: deps.api.addr_validate(&msg.core_contract)?, + withdrawal_token_contract: deps.api.addr_validate(&msg.token_contract)?, withdrawal_voucher_contract: deps.api.addr_validate(&msg.voucher_contract)?, base_denom: msg.base_denom, }, @@ -97,6 +98,9 @@ pub fn execute( } } } + ExecuteMsg::ReceiveWithdrawalDenoms { receiver } => { + execute_receive_withdrawal_denoms(deps, info, receiver) + } ExecuteMsg::Pause {} => exec_pause(deps, info), ExecuteMsg::Unpause {} => exec_unpause(deps, info), } @@ -237,6 +241,106 @@ fn execute_receive_nft_withdraw( Ok(response("execute-receive_nft", CONTRACT_NAME, attrs).add_messages(messages)) } +fn execute_receive_withdrawal_denoms( + deps: DepsMut, + info: MessageInfo, + receiver: Option, +) -> ContractResult> { + pause_guard(deps.storage)?; + + let mut attrs = vec![attr("action", "receive_withdrawal_denoms")]; + let config = CONFIG.load(deps.storage)?; + + let withdrawn_coin = cw_utils::one_coin(&info)?; + let Coin { amount, denom } = withdrawn_coin.clone(); + let batch_id = get_batch_id_by_withdrawal_denom(denom, &config)?; + + let unbond_batch: UnbondBatch = deps.querier.query_wasm_smart( + &config.core_contract, + &drop_staking_base::msg::core::QueryMsg::UnbondBatch { + batch_id: batch_id.into(), + }, + )?; + ensure_eq!( + unbond_batch.status, + UnbondBatchStatus::Withdrawn, + ContractError::BatchIsNotWithdrawn {} + ); + + let user_share = Decimal::from_ratio(amount, unbond_batch.total_dasset_amount_to_withdraw); + + let payout_amount = user_share * unbond_batch.unbonded_amount.unwrap_or(Uint128::zero()); + let to_address = receiver.unwrap_or(info.sender.to_string()); + attrs.push(attr("batch_id", batch_id.to_string())); + attrs.push(attr("payout_amount", payout_amount.to_string())); + attrs.push(attr("to_address", &to_address)); + + let messages = vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.withdrawal_token_contract.to_string(), + msg: to_json_binary( + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::Burn { + batch_id: Uint128::from(batch_id), + }, + )?, + funds: info.funds, + }), + CosmosMsg::Bank(BankMsg::Send { + to_address, + amount: vec![Coin { + denom: config.base_denom, + amount: payout_amount, + }], + }), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.core_contract.to_string(), + msg: to_json_binary( + &drop_staking_base::msg::core::ExecuteMsg::UpdateWithdrawnAmount { + batch_id, + withdrawn_amount: payout_amount, + }, + )?, + funds: vec![], + }), + ]; + + Ok(response("execute-receive_withdrawal_denoms", CONTRACT_NAME, attrs).add_messages(messages)) +} + +fn get_batch_id_by_withdrawal_denom( + withdrawal_denom: String, + config: &Config, +) -> Result { + let tokenfactory_denom_parts: Vec<&str> = withdrawal_denom.split('/').collect(); + + if tokenfactory_denom_parts.len() != 3 { + return Err(ContractError::InvalidDenom {}); + } + + let prefix = tokenfactory_denom_parts[0]; + let creator_address = tokenfactory_denom_parts[1]; + let subdenom = tokenfactory_denom_parts[2]; + + if !prefix.eq_ignore_ascii_case("factory") { + return Err(ContractError::InvalidDenom {}); + } + + if !creator_address.eq_ignore_ascii_case(config.withdrawal_token_contract.as_ref()) { + return Err(ContractError::InvalidDenom {}); + } + + let subdenom_parts: Vec<&str> = subdenom.split(':').collect(); + if subdenom_parts.get(2).is_none() { + return Err(ContractError::InvalidDenom {}); + } + let batch_id = subdenom_parts[2]; + + match batch_id.parse::() { + Ok(value) => Ok(value), + Err(_) => Err(ContractError::InvalidDenom {}), + } +} + #[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] pub fn migrate( deps: DepsMut, diff --git a/contracts/withdrawal-manager/src/error.rs b/contracts/withdrawal-manager/src/error.rs index c51f02cfc..a930c3809 100644 --- a/contracts/withdrawal-manager/src/error.rs +++ b/contracts/withdrawal-manager/src/error.rs @@ -12,9 +12,15 @@ pub enum ContractError { #[error("{0}")] NeutronError(#[from] NeutronError), + #[error("{0}")] + PaymentError(#[from] cw_utils::PaymentError), + #[error("Invalid NFT: {reason}")] InvalidNFT { reason: String }, + #[error("Invalid denom")] + InvalidDenom {}, + #[error("{0}")] OverflowError(#[from] OverflowError), diff --git a/contracts/withdrawal-manager/src/lib.rs b/contracts/withdrawal-manager/src/lib.rs index 77a79e559..247a6a8be 100644 --- a/contracts/withdrawal-manager/src/lib.rs +++ b/contracts/withdrawal-manager/src/lib.rs @@ -1,2 +1,4 @@ pub mod contract; mod error; +#[cfg(test)] +mod tests; diff --git a/contracts/withdrawal-manager/src/tests.rs b/contracts/withdrawal-manager/src/tests.rs new file mode 100644 index 000000000..7de7af0e9 --- /dev/null +++ b/contracts/withdrawal-manager/src/tests.rs @@ -0,0 +1,246 @@ +use crate::contract::execute; +use crate::error::ContractError; +use cosmwasm_std::{ + testing::{mock_env, mock_info}, + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Event, Response, SubMsg, Uint128, WasmMsg, +}; +use drop_helpers::testing::mock_dependencies; +use drop_staking_base::state::core::{UnbondBatch, UnbondBatchStatus, UnbondBatchStatusTimestamps}; +use drop_staking_base::{ + msg::withdrawal_manager::ExecuteMsg, + state::withdrawal_manager::{Config, CONFIG}, +}; + +fn get_default_config() -> Config { + Config { + core_contract: Addr::unchecked("core_contract"), + withdrawal_token_contract: Addr::unchecked("withdrawal_token_contract"), + withdrawal_voucher_contract: Addr::unchecked("withdrawal_voucher_contract"), + base_denom: "base_denom".to_string(), + } +} + +fn get_default_unbond_batch_status_timestamps() -> UnbondBatchStatusTimestamps { + UnbondBatchStatusTimestamps { + new: 0, + unbond_requested: None, + unbond_failed: None, + unbonding: None, + withdrawing: None, + withdrawn: None, + withdrawing_emergency: None, + withdrawn_emergency: None, + } +} + +#[test] +fn test_receive_withdrawal_denoms_happy_path() { + let mut deps = mock_dependencies(&[]); + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + deps.querier.add_wasm_query_response("core_contract", |_| { + to_json_binary(&UnbondBatch { + total_dasset_amount_to_withdraw: Uint128::from(1001u128), + expected_native_asset_amount: Uint128::from(1001u128), + total_unbond_items: 1, + status: UnbondBatchStatus::Withdrawn, + expected_release_time: 9000, + slashing_effect: None, + unbonded_amount: Option::from(Uint128::new(1000u128)), + withdrawn_amount: None, + status_timestamps: get_default_unbond_batch_status_timestamps(), + }) + .unwrap() + }); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info( + "any sender", + &[Coin::new( + 1000, + "factory/withdrawal_token_contract/dATOM:unbond:0", + )], + ), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ) + .unwrap(); + + assert_eq!( + res, + Response::new() + .add_submessages([SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "withdrawal_token_contract".to_string(), + msg: to_json_binary( + &drop_staking_base::msg::withdrawal_token::ExecuteMsg::Burn { + batch_id: Uint128::zero(), + }, + ).unwrap(), + funds: vec![Coin::new(1000, "factory/withdrawal_token_contract/dATOM:unbond:0")], + })), + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "any sender".to_string(), + amount: vec![Coin { + denom: "base_denom".to_string(), + amount: Uint128::from(999u128), + }], + })), + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "core_contract".to_string(), + msg: to_json_binary( + &drop_staking_base::msg::core::ExecuteMsg::UpdateWithdrawnAmount { + batch_id: 0u128, + withdrawn_amount: Uint128::from(999u128), + }, + ).unwrap(), + funds: vec![], + }))]) + .add_event( + Event::new("crates.io:drop-staking__drop-withdrawal-manager-execute-receive_withdrawal_denoms").add_attributes( + vec![ + ("action", "receive_withdrawal_denoms"), + ("batch_id", "0"), + ("payout_amount", "999"), + ("to_address", "any sender") + ] + ) + ) + ); +} + +#[test] +fn test_receive_withdrawal_denoms_has_few_parts() { + let mut deps = mock_dependencies(&[]); + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info("any sender", &[Coin::new(1000, "factory/dATOM:unbond:0")]), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ); + assert!(res.is_err()); + assert_eq!(res, Err(ContractError::InvalidDenom {})); +} + +#[test] +fn test_receive_withdrawal_denoms_has_incorrect_prefix() { + let mut deps = mock_dependencies(&[]); + let denom = "invalid/withdrawal_token_contract/dATOM:unbond:0"; + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info("any sender", &[Coin::new(1000, denom)]), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ); + assert!(res.is_err()); + assert_eq!(res, Err(ContractError::InvalidDenom {})); +} + +#[test] +fn test_receive_withdrawal_denoms_has_incorrect_owner() { + let mut deps = mock_dependencies(&[]); + let denom = "factory/invalid/dATOM:unbond:0"; + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info("any sender", &[Coin::new(1000, denom)]), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ); + assert!(res.is_err()); + assert_eq!(res, Err(ContractError::InvalidDenom {})); +} + +#[test] +fn test_receive_withdrawal_denoms_has_incorrect_subdenom() { + let mut deps = mock_dependencies(&[]); + let denom = "factory/withdrawal_token_contract/invalid"; + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info("any sender", &[Coin::new(1000, denom)]), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ); + assert!(res.is_err()); + assert_eq!(res, Err(ContractError::InvalidDenom {})); +} + +#[test] +fn test_receive_withdrawal_denoms_has_incorrect_subdenom_batch_id() { + let mut deps = mock_dependencies(&[]); + let denom = "factory/withdrawal_token_contract/dATOM:unbond:invalid"; + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info("any sender", &[Coin::new(1000, denom)]), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ); + assert!(res.is_err()); + assert_eq!(res, Err(ContractError::InvalidDenom {})); +} + +#[test] +fn test_receive_withdrawal_denoms_batch_not_withdrawn() { + let mut deps = mock_dependencies(&[]); + + CONFIG + .save(deps.as_mut().storage, &get_default_config()) + .unwrap(); + + deps.querier.add_wasm_query_response("core_contract", |_| { + to_json_binary(&UnbondBatch { + total_dasset_amount_to_withdraw: Uint128::from(1001u128), + expected_native_asset_amount: Uint128::from(1001u128), + total_unbond_items: 1, + status: UnbondBatchStatus::Unbonding, + expected_release_time: 9000, + slashing_effect: None, + unbonded_amount: Option::from(Uint128::new(1000u128)), + withdrawn_amount: None, + status_timestamps: get_default_unbond_batch_status_timestamps(), + }) + .unwrap() + }); + + let res = execute( + deps.as_mut(), + mock_env().clone(), + mock_info( + "any sender", + &[Coin::new( + 1000, + "factory/withdrawal_token_contract/dATOM:unbond:0", + )], + ), + ExecuteMsg::ReceiveWithdrawalDenoms { receiver: None }, + ); + assert!(res.is_err()); + assert_eq!(res, Err(ContractError::BatchIsNotWithdrawn {})); +} diff --git a/contracts/withdrawal-token/.cargo/config b/contracts/withdrawal-token/.cargo/config new file mode 100644 index 000000000..2dc2b8401 --- /dev/null +++ b/contracts/withdrawal-token/.cargo/config @@ -0,0 +1,2 @@ +[alias] +schema = "run --bin drop-withdrawal-token-schema" \ No newline at end of file diff --git a/contracts/withdrawal-token/Cargo.toml b/contracts/withdrawal-token/Cargo.toml new file mode 100644 index 000000000..3ff9e93c8 --- /dev/null +++ b/contracts/withdrawal-token/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = ["Denis Zagumennov "] +description = "Contract which mints and burns withdrawal tokens" +edition = "2021" +name = "drop-withdrawal-token" +version = "1.0.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +drop-staking-base = { workspace = true } +drop-helpers = { workspace = true } +cosmwasm-schema = { workspace = true } +cw2 = { workspace = true } +cw-utils = { workspace = true } +neutron-sdk = { workspace = true } +thiserror = { workspace = true } +cw-ownable = { workspace = true } +cosmos-sdk-proto = { workspace = true } +semver = { workspace = true } diff --git a/contracts/withdrawal-token/README.md b/contracts/withdrawal-token/README.md new file mode 100644 index 000000000..8c2b8ee55 --- /dev/null +++ b/contracts/withdrawal-token/README.md @@ -0,0 +1 @@ +# DROP Withdrawal Token \ No newline at end of file diff --git a/contracts/withdrawal-token/src/bin/drop-withdrawal-token-schema.rs b/contracts/withdrawal-token/src/bin/drop-withdrawal-token-schema.rs new file mode 100644 index 000000000..382755106 --- /dev/null +++ b/contracts/withdrawal-token/src/bin/drop-withdrawal-token-schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use drop_staking_base::msg::withdrawal_token::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/withdrawal-token/src/contract.rs b/contracts/withdrawal-token/src/contract.rs new file mode 100644 index 000000000..6ccdc69b2 --- /dev/null +++ b/contracts/withdrawal-token/src/contract.rs @@ -0,0 +1,209 @@ +use cosmwasm_std::{ + attr, ensure_eq, ensure_ne, entry_point, to_json_binary, Addr, Binary, Deps, DepsMut, Env, + MessageInfo, Reply, Response, SubMsg, Uint128, +}; +use drop_helpers::answer::{attr_coin, response}; +use drop_staking_base::{ + error::withdrawal_token::{ContractError, ContractResult}, + msg::withdrawal_token::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + state::withdrawal_token::{CORE_ADDRESS, DENOM_PREFIX, WITHDRAWAL_MANAGER_ADDRESS}, +}; +use neutron_sdk::{ + bindings::{msg::NeutronMsg, query::NeutronQuery}, + query::token_factory::query_full_denom, +}; +use std::fmt::Display; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CREATE_DENOM_REPLY_ID: u64 = 1; +pub const UNBOND_MARK: &str = "unbond"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult> { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.owner))?; + + let core = deps.api.addr_validate(&msg.core_address)?; + CORE_ADDRESS.save(deps.storage, &core)?; + + let withdrawal_manager = deps.api.addr_validate(&msg.withdrawal_manager_address)?; + WITHDRAWAL_MANAGER_ADDRESS.save(deps.storage, &withdrawal_manager)?; + + DENOM_PREFIX.save(deps.storage, &msg.denom_prefix)?; + + Ok(response( + "instantiate", + CONTRACT_NAME, + [ + attr("core_address", core), + attr("withdrawal_manager_address", withdrawal_manager), + ], + )) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult> { + match msg { + ExecuteMsg::UpdateOwnership(action) => { + cw_ownable::update_ownership(deps.into_empty(), &env.block, &info.sender, action)?; + Ok(response::<(&str, &str), _>( + "execute-update-ownership", + CONTRACT_NAME, + [], + )) + } + ExecuteMsg::CreateDenom { batch_id } => create_denom(deps, info, batch_id), + ExecuteMsg::Mint { + amount, + receiver, + batch_id, + } => mint(deps, info, env, amount, receiver, batch_id), + ExecuteMsg::Burn { batch_id } => burn(deps, info, env, batch_id), + } +} + +fn create_denom( + deps: DepsMut, + info: MessageInfo, + batch_id: Uint128, +) -> ContractResult> { + let core = CORE_ADDRESS.load(deps.storage)?; + ensure_eq!(info.sender, core, ContractError::Unauthorized); + + let denom_prefix = DENOM_PREFIX.load(deps.storage)?; + let subdenom = build_subdenom_name(denom_prefix, batch_id); + + let create_denom_msg = SubMsg::reply_on_success( + NeutronMsg::submit_create_denom(&subdenom), + CREATE_DENOM_REPLY_ID, + ); + + Ok(response( + "execute-create-denom", + CONTRACT_NAME, + [attr("batch_id", batch_id), attr("subdenom", subdenom)], + ) + .add_submessage(create_denom_msg)) +} + +fn mint( + deps: DepsMut, + info: MessageInfo, + env: Env, + amount: Uint128, + receiver: String, + batch_id: Uint128, +) -> ContractResult> { + ensure_ne!(amount, Uint128::zero(), ContractError::NothingToMint); + + let core = CORE_ADDRESS.load(deps.storage)?; + ensure_eq!(info.sender, core, ContractError::Unauthorized); + + let denom_prefix = DENOM_PREFIX.load(deps.storage)?; + let subdenom = build_subdenom_name(denom_prefix, batch_id); + let full_denom = query_full_denom(deps.as_ref(), env.contract.address, subdenom)?; + let mint_msg = NeutronMsg::submit_mint_tokens(&full_denom.denom, amount, &receiver); + + Ok(response( + "execute-mint", + CONTRACT_NAME, + [ + attr("amount", amount), + attr("denom", full_denom.denom), + attr("receiver", receiver), + attr("batch_id", batch_id.to_string()), + ], + ) + .add_message(mint_msg)) +} + +fn burn( + deps: DepsMut, + info: MessageInfo, + env: Env, + batch_id: Uint128, +) -> ContractResult> { + let withdrawal_manager = WITHDRAWAL_MANAGER_ADDRESS.load(deps.storage)?; + ensure_eq!(info.sender, withdrawal_manager, ContractError::Unauthorized); + + let denom_prefix = DENOM_PREFIX.load(deps.storage)?; + let subdenom = build_subdenom_name(denom_prefix, batch_id); + let full_denom = query_full_denom(deps.as_ref(), env.contract.address, subdenom)?; + + let amount = cw_utils::must_pay(&info, &full_denom.denom)?; + let burn_msg = NeutronMsg::submit_burn_tokens(&full_denom.denom, amount); + + Ok(response( + "execute-burn", + CONTRACT_NAME, + [attr_coin("amount", amount, full_denom.denom)], + ) + .add_message(burn_msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::Ownership {} => Ok(to_json_binary( + &cw_ownable::get_ownership(deps.storage)? + .owner + .unwrap_or(Addr::unchecked("")) + .to_string(), + )?), + QueryMsg::Config {} => { + let core_address = CORE_ADDRESS.load(deps.storage)?.into_string(); + let withdrawal_manager_address = + WITHDRAWAL_MANAGER_ADDRESS.load(deps.storage)?.into_string(); + let denom_prefix = DENOM_PREFIX.load(deps.storage)?; + Ok(to_json_binary(&ConfigResponse { + core_address, + withdrawal_manager_address, + denom_prefix, + })?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> ContractResult> { + let version: semver::Version = CONTRACT_VERSION.parse()?; + let storage_version: semver::Version = + cw2::get_contract_version(deps.storage)?.version.parse()?; + + if storage_version < version { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + } + + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + _deps: DepsMut, + _env: Env, + msg: Reply, +) -> ContractResult> { + match msg.id { + CREATE_DENOM_REPLY_ID => Ok(response( + "reply-create-denom", + CONTRACT_NAME, + [attr("denom", "new unbond denom")], + )), + id => Err(ContractError::UnknownReplyId { id }), + } +} + +fn build_subdenom_name(denom_prefix: impl Display, batch_id: impl Display) -> String { + format!("{denom_prefix}:{UNBOND_MARK}:{batch_id}") +} diff --git a/contracts/withdrawal-token/src/lib.rs b/contracts/withdrawal-token/src/lib.rs new file mode 100644 index 000000000..b8c7dc83c --- /dev/null +++ b/contracts/withdrawal-token/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +#[cfg(test)] +mod tests; diff --git a/contracts/withdrawal-token/src/tests.rs b/contracts/withdrawal-token/src/tests.rs new file mode 100644 index 000000000..e1a28067d --- /dev/null +++ b/contracts/withdrawal-token/src/tests.rs @@ -0,0 +1,628 @@ +use crate::contract::{execute, query, reply, CREATE_DENOM_REPLY_ID, UNBOND_MARK}; +use cosmwasm_std::{ + attr, coin, from_json, + testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}, + to_json_binary, Addr, CosmosMsg, Event, QueryRequest, Reply, ReplyOn, Response, SubMsgResult, + Uint128, +}; +use drop_helpers::testing::mock_dependencies; +use drop_staking_base::error::withdrawal_token::ContractError; +use drop_staking_base::msg::withdrawal_token::{ + ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, +}; +use drop_staking_base::state::withdrawal_token::{ + CORE_ADDRESS, DENOM_PREFIX, WITHDRAWAL_MANAGER_ADDRESS, +}; +use neutron_sdk::bindings::msg::NeutronMsg; +use neutron_sdk::bindings::query::NeutronQuery; +use neutron_sdk::query::token_factory::FullDenomResponse; + +#[test] +fn test_instantiate() { + let mut deps = mock_dependencies(&[]); + let msg = InstantiateMsg { + core_address: "core_contract".to_string(), + withdrawal_manager_address: "withdrawal_manager_contract".to_string(), + denom_prefix: "denom".to_string(), + owner: "owner".to_string(), + }; + let env = mock_env(); + let res = + crate::contract::instantiate(deps.as_mut(), env, mock_info("sender", &[]), msg).unwrap(); + assert_eq!( + res, + Response::new().add_event( + Event::new("drop-withdrawal-token-instantiate").add_attributes(vec![ + ("core_address", "core_contract"), + ("withdrawal_manager_address", "withdrawal_manager_contract") + ]) + ) + ); + assert_eq!( + Addr::unchecked("owner"), + cw_ownable::get_ownership(deps.as_mut().storage) + .unwrap() + .owner + .unwrap() + ); +} + +#[test] +fn test_query_ownership() { + let mut deps = mock_dependencies(&[]); + let deps_mut = deps.as_mut(); + cw_ownable::initialize_owner(deps_mut.storage, deps_mut.api, Some("owner")).unwrap(); + assert_eq!( + from_json::(query(deps.as_ref(), mock_env(), QueryMsg::Ownership {}).unwrap()) + .unwrap(), + String::from("owner"), + ); +} + +#[test] +fn test_query_config() { + let mut deps = mock_dependencies(&[]); + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + let response = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + assert_eq!( + response, + to_json_binary(&ConfigResponse { + core_address: "core_contract".to_string(), + withdrawal_manager_address: "withdrawal_manager_contract".to_string(), + denom_prefix: "denom_prefix".to_string() + }) + .unwrap() + ); +} + +#[test] +fn test_create_denom() { + let mut deps = mock_dependencies(&[]); + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + let subdenom = format!("denom_prefix:{}:0", UNBOND_MARK); + let response = execute( + deps.as_mut(), + mock_env(), + mock_info("core_contract", &[]), + ExecuteMsg::CreateDenom { + batch_id: Uint128::zero(), + }, + ) + .unwrap(); + assert_eq!(response.messages.len(), 1); + assert_eq!(response.messages[0].reply_on, ReplyOn::Success); + assert_eq!(response.messages[0].id, CREATE_DENOM_REPLY_ID); + assert_eq!( + response.messages[0].msg, + CosmosMsg::Custom(NeutronMsg::CreateDenom { + subdenom: subdenom.to_string(), + }) + ); + assert_eq!( + response.events, + vec![ + Event::new("drop-withdrawal-token-execute-create-denom").add_attributes([ + attr("batch_id", "0"), + attr("subdenom", subdenom.to_string()) + ]) + ] + ); + assert!(response.attributes.is_empty()); +} + +#[test] +fn reply_unknown_id() { + let mut deps = mock_dependencies(&[]); + let error = reply( + deps.as_mut(), + mock_env(), + Reply { + id: 512, + result: SubMsgResult::Err("".to_string()), + }, + ) + .unwrap_err(); + assert_eq!(error, ContractError::UnknownReplyId { id: 512 }); +} + +#[test] +fn test_reply() { + let mut deps = mock_dependencies(&[]); + + let response = reply( + deps.as_mut(), + mock_env(), + Reply { + id: CREATE_DENOM_REPLY_ID, + result: SubMsgResult::Err("".to_string()), + }, + ) + .unwrap(); + + assert_eq!( + response.events, + vec![Event::new("drop-withdrawal-token-reply-create-denom") + .add_attributes([attr("denom", "new unbond denom")])] + ); + assert!(response.attributes.is_empty()); +} + +#[test] +fn test_mint_zero() { + let mut deps = mock_dependencies(&[]); + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + let error = execute( + deps.as_mut(), + mock_env(), + mock_info("core", &[]), + ExecuteMsg::Mint { + amount: Uint128::zero(), + receiver: "receiver".to_string(), + batch_id: Uint128::zero(), + }, + ) + .unwrap_err(); + assert_eq!(error, ContractError::NothingToMint); +} + +#[test] +fn test_mint() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + deps.querier + .add_custom_query_response(|request| match request { + QueryRequest::Custom(NeutronQuery::FullDenom { + creator_addr, + subdenom, + }) => { + assert_eq!(creator_addr, MOCK_CONTRACT_ADDR); + assert_eq!(subdenom, &format!("denom_prefix:{}:0", UNBOND_MARK)); + to_json_binary(&FullDenomResponse { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + }) + .unwrap() + } + _ => unimplemented!(), + }); + + let response = execute( + deps.as_mut(), + mock_env(), + mock_info("core_contract", &[]), + ExecuteMsg::Mint { + amount: Uint128::new(228), + receiver: "receiver".to_string(), + batch_id: Uint128::zero(), + }, + ) + .unwrap(); + + assert_eq!(response.messages.len(), 1); + assert_eq!( + response.messages[0].msg, + CosmosMsg::Custom(NeutronMsg::MintTokens { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + amount: Uint128::new(228), + mint_to_address: "receiver".to_string(), + }) + ); + assert_eq!( + response.events, + vec![ + Event::new("drop-withdrawal-token-execute-mint").add_attributes([ + attr("amount", "228"), + attr( + "denom", + format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string() + ), + attr("receiver", "receiver"), + attr("batch_id", "0"), + ]) + ] + ); + assert!(response.attributes.is_empty()); +} + +#[test] +fn mint_stranger() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + let error = execute( + deps.as_mut(), + mock_env(), + mock_info("stranger", &[]), + ExecuteMsg::Mint { + amount: Uint128::new(220), + receiver: "receiver".to_string(), + batch_id: Uint128::zero(), + }, + ) + .unwrap_err(); + + assert_eq!(error, ContractError::Unauthorized); +} + +#[test] +fn burn_zero() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + deps.querier + .add_custom_query_response(|request| match request { + QueryRequest::Custom(NeutronQuery::FullDenom { + creator_addr, + subdenom, + }) => { + assert_eq!(creator_addr, MOCK_CONTRACT_ADDR); + assert_eq!(subdenom, &format!("denom_prefix:{}:0", UNBOND_MARK)); + to_json_binary(&FullDenomResponse { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + }) + .unwrap() + } + _ => unimplemented!(), + }); + + let error = execute( + deps.as_mut(), + mock_env(), + mock_info("withdrawal_manager_contract", &[]), + ExecuteMsg::Burn { + batch_id: Uint128::zero(), + }, + ) + .unwrap_err(); + assert_eq!( + error, + ContractError::PaymentError(cw_utils::PaymentError::NoFunds {}) + ); +} + +#[test] +fn burn_multiple_coins() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + deps.querier + .add_custom_query_response(|request| match request { + QueryRequest::Custom(NeutronQuery::FullDenom { + creator_addr, + subdenom, + }) => { + assert_eq!(creator_addr, MOCK_CONTRACT_ADDR); + assert_eq!(subdenom, &format!("denom_prefix:{}:0", UNBOND_MARK)); + to_json_binary(&FullDenomResponse { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + }) + .unwrap() + } + _ => unimplemented!(), + }); + + let error = execute( + deps.as_mut(), + mock_env(), + mock_info( + "withdrawal_manager_contract", + &[coin(20, "coin1"), coin(10, "denom")], + ), + ExecuteMsg::Burn { + batch_id: Uint128::zero(), + }, + ) + .unwrap_err(); + assert_eq!( + error, + ContractError::PaymentError(cw_utils::PaymentError::MultipleDenoms {}) + ); +} + +#[test] +fn burn_invalid_coin() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + deps.querier + .add_custom_query_response(|request| match request { + QueryRequest::Custom(NeutronQuery::FullDenom { + creator_addr, + subdenom, + }) => { + assert_eq!(creator_addr, MOCK_CONTRACT_ADDR); + assert_eq!(subdenom, &format!("denom_prefix:{}:0", UNBOND_MARK)); + to_json_binary(&FullDenomResponse { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + }) + .unwrap() + } + _ => unimplemented!(), + }); + + let error = execute( + deps.as_mut(), + mock_env(), + mock_info("withdrawal_manager_contract", &[coin(20, "not_that_coin")]), + ExecuteMsg::Burn { + batch_id: Uint128::zero(), + }, + ) + .unwrap_err(); + assert_eq!( + error, + ContractError::PaymentError(cw_utils::PaymentError::MissingDenom( + format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string() + )) + ); +} + +#[test] +fn burn_stranger() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + deps.querier + .add_custom_query_response(|request| match request { + QueryRequest::Custom(NeutronQuery::FullDenom { + creator_addr, + subdenom, + }) => { + assert_eq!(creator_addr, MOCK_CONTRACT_ADDR); + assert_eq!(subdenom, &format!("denom_prefix:{}:0", UNBOND_MARK)); + to_json_binary(&FullDenomResponse { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + }) + .unwrap() + } + _ => unimplemented!(), + }); + + let error = execute( + deps.as_mut(), + mock_env(), + mock_info( + "stranger", + &[coin( + 160, + format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + )], + ), + ExecuteMsg::Burn { + batch_id: Uint128::zero(), + }, + ) + .unwrap_err(); + + assert_eq!(error, ContractError::Unauthorized); +} + +#[test] +fn burn() { + let mut deps = mock_dependencies(&[]); + + CORE_ADDRESS + .save(deps.as_mut().storage, &Addr::unchecked("core_contract")) + .unwrap(); + WITHDRAWAL_MANAGER_ADDRESS + .save( + deps.as_mut().storage, + &Addr::unchecked("withdrawal_manager_contract"), + ) + .unwrap(); + DENOM_PREFIX + .save(deps.as_mut().storage, &String::from("denom_prefix")) + .unwrap(); + + deps.querier + .add_custom_query_response(|request| match request { + QueryRequest::Custom(NeutronQuery::FullDenom { + creator_addr, + subdenom, + }) => { + assert_eq!(creator_addr, MOCK_CONTRACT_ADDR); + assert_eq!(subdenom, &format!("denom_prefix:{}:0", UNBOND_MARK)); + to_json_binary(&FullDenomResponse { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + }) + .unwrap() + } + _ => unimplemented!(), + }); + + let response = execute( + deps.as_mut(), + mock_env(), + mock_info( + "withdrawal_manager_contract", + &[coin( + 144, + format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + )], + ), + ExecuteMsg::Burn { + batch_id: Uint128::zero(), + }, + ) + .unwrap(); + + assert_eq!(response.messages.len(), 1); + assert_eq!( + response.messages[0].msg, + CosmosMsg::Custom(NeutronMsg::BurnTokens { + denom: format!( + "factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string(), + amount: Uint128::new(144), + burn_from_address: "".to_string(), + }) + ); + assert_eq!( + response.events, + vec![ + Event::new("drop-withdrawal-token-execute-burn").add_attributes([attr( + "amount", + format!( + "144factory/{}/denom_prefix:{}:0", + MOCK_CONTRACT_ADDR, UNBOND_MARK + ) + .to_string() + )]) + ] + ); + assert!(response.attributes.is_empty()); +} diff --git a/integration_tests/src/testcases/auto-withdrawer.test.ts b/integration_tests/src/testcases/auto-withdrawer.test.ts index d100ee468..e58c39fce 100644 --- a/integration_tests/src/testcases/auto-withdrawer.test.ts +++ b/integration_tests/src/testcases/auto-withdrawer.test.ts @@ -8,6 +8,7 @@ import { DropStrategy, DropStaker, DropWithdrawalManager, + DropWithdrawalToken, DropWithdrawalVoucher, DropSplitter, DropToken, @@ -45,6 +46,7 @@ const DropPumpClass = DropPump.Client; const DropPuppeteerClass = DropPuppeteer.Client; const DropStrategyClass = DropStrategy.Client; const DropStakerClass = DropStaker.Client; +const DropWithdrawalTokenClass = DropWithdrawalToken.Client; const DropWithdrawalVoucherClass = DropWithdrawalVoucher.Client; const DropWithdrawalManagerClass = DropWithdrawalManager.Client; const DropAutoWithdrawerClass = DropAutoWithdrawer.Client; @@ -68,6 +70,9 @@ describe('Auto withdrawer', () => { rewardsPumpContractClient?: InstanceType; puppeteerContractClient?: InstanceType; tokenContractClient?: InstanceType; + withdrawalTokenContractClient?: InstanceType< + typeof DropWithdrawalTokenClass + >; withdrawalVoucherContractClient?: InstanceType< typeof DropWithdrawalVoucherClass >; @@ -94,6 +99,7 @@ describe('Auto withdrawer', () => { codeIds: { core?: number; token?: number; + withdrawalToken?: number; withdrawalVoucher?: number; withdrawalManager?: number; strategy?: number; @@ -108,6 +114,7 @@ describe('Auto withdrawer', () => { exchangeRate?: number; neutronIBCDenom?: string; ldDenom?: string; + withdrawalDenom?: (string) => string; } = { codeIds: {} }; beforeAll(async (t) => { @@ -281,6 +288,17 @@ describe('Auto withdrawer', () => { expect(res.codeId).toBeGreaterThan(0); context.codeIds.token = res.codeId; } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_withdrawal_token.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.withdrawalToken = res.codeId; + } { const res = await client.upload( account.address, @@ -402,6 +420,7 @@ describe('Auto withdrawer', () => { code_ids: { core_code_id: context.codeIds.core, token_code_id: context.codeIds.token, + withdrawal_token_code_id: context.codeIds.withdrawalToken, withdrawal_voucher_code_id: context.codeIds.withdrawalVoucher, withdrawal_manager_code_id: context.codeIds.withdrawalManager, strategy_code_id: context.codeIds.strategy, @@ -482,6 +501,13 @@ describe('Auto withdrawer', () => { res.core_contract, ); expect(coreContractInfo.data.contract_info.label).toBe('drop-staking-core'); + const withdrawalTokenContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.withdrawal_token_contract, + ); + expect(withdrawalTokenContractInfo.data.contract_info.label).toBe( + 'drop-staking-withdrawal-token', + ); const withdrawalVoucherContractInfo = await neutronClient.CosmwasmWasmV1.query.queryContractInfo( res.withdrawal_voucher_contract, @@ -506,6 +532,10 @@ describe('Auto withdrawer', () => { context.coreContractClient = instrumentCoreClass( new DropCore.Client(context.client, res.core_contract), ); + context.withdrawalTokenContractClient = new DropWithdrawalToken.Client( + context.client, + res.withdrawal_token_contract, + ); context.withdrawalVoucherContractClient = new DropWithdrawalVoucher.Client( context.client, res.withdrawal_voucher_contract, @@ -539,6 +569,8 @@ describe('Auto withdrawer', () => { res.token_contract, ); context.ldDenom = `factory/${res.token_contract}/drop`; + context.withdrawalDenom = (batchId) => + `factory/${res.withdrawal_token_contract}/drop:unbond:${batchId}`; }); it('setup ICA for rewards pump', async () => { @@ -805,11 +837,12 @@ describe('Auto withdrawer', () => { res.codeId, { core_address: context.coreContractClient.contractAddress, - withdrawal_voucher_address: - context.withdrawalVoucherContractClient.contractAddress, + withdrawal_token_address: + context.withdrawalTokenContractClient.contractAddress, withdrawal_manager_address: context.withdrawalManagerContractClient.contractAddress, ld_token: ldDenom, + withdrawal_denom_prefix: 'drop', }, 'drop-auto-withdrawer', 'auto', @@ -850,6 +883,7 @@ describe('Auto withdrawer', () => { expect(bondings).toEqual({ bondings: [ { + bonding_id: `${neutronUserAddress}_0`, bonder: neutronUserAddress, deposit: [ { @@ -857,7 +891,7 @@ describe('Auto withdrawer', () => { denom: 'untrn', }, ], - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + withdrawal_amount: '20000', }, ], next_page_key: null, @@ -866,15 +900,12 @@ describe('Auto withdrawer', () => { await checkExchangeRate(context); }); it('unbond', async () => { - const { - neutronUserAddress, - autoWithdrawerContractClient, - withdrawalVoucherContractClient, - } = context; + const { neutronClient, neutronUserAddress, autoWithdrawerContractClient } = + context; const res = await autoWithdrawerContractClient.unbond( neutronUserAddress, { - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + batch_id: '0', }, 1.6, undefined, @@ -882,10 +913,18 @@ describe('Auto withdrawer', () => { ); expect(res.transactionHash).toHaveLength(64); - const owner = await withdrawalVoucherContractClient.queryOwnerOf({ - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + const balances = + await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronUserAddress, + ); + expect( + balances.data.balances.find( + (one) => one.denom === context.withdrawalDenom('0'), + ), + ).toEqual({ + denom: context.withdrawalDenom('0'), + amount: '520000', }); - expect(owner.owner).toEqual(neutronUserAddress); const bondings = await autoWithdrawerContractClient.queryBondings({ user: neutronUserAddress, @@ -896,37 +935,23 @@ describe('Auto withdrawer', () => { }); await checkExchangeRate(context); }); - it('bond with NFT', async () => { - const { - neutronUserAddress, - autoWithdrawerContractClient, - withdrawalVoucherContractClient, - } = context; + it('bond with withdrawal denoms', async () => { + const { neutronUserAddress, autoWithdrawerContractClient } = context; - { - const res = await withdrawalVoucherContractClient.approve( - neutronUserAddress, - { - spender: autoWithdrawerContractClient.contractAddress, - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, - }, - 1.6, - undefined, - [], - ); - expect(res.transactionHash).toHaveLength(64); - } { const res = await autoWithdrawerContractClient.bond( neutronUserAddress, { - with_n_f_t: { - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + with_withdrawal_denoms: { + batch_id: '0', }, }, 1.6, undefined, - [{ amount: '40000', denom: 'untrn' }], + [ + { amount: '20000', denom: context.withdrawalDenom('0') }, + { amount: '40000', denom: 'untrn' }, + ], ); expect(res.transactionHash).toHaveLength(64); } @@ -937,6 +962,7 @@ describe('Auto withdrawer', () => { expect(bondings).toEqual({ bondings: [ { + bonding_id: `${neutronUserAddress}_0`, bonder: neutronUserAddress, deposit: [ { @@ -944,7 +970,7 @@ describe('Auto withdrawer', () => { denom: 'untrn', }, ], - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + withdrawal_amount: '20000', }, ], next_page_key: null, @@ -1425,14 +1451,14 @@ describe('Auto withdrawer', () => { return balances.data.balances.length > 0; }, 200_000); }); - it('withdraw', async () => { + it('withdraw partial amount', async () => { const { neutronUserAddress, neutronClient, neutronIBCDenom, autoWithdrawerContractClient, } = context; - const expectedWithdrawnAmount = 20000; + const expectedWithdrawnAmount = 5000; const balanceBefore = parseInt( ( @@ -1446,7 +1472,8 @@ describe('Auto withdrawer', () => { const res = await autoWithdrawerContractClient.withdraw( neutronUserAddress, { - token_id: `0_${autoWithdrawerContractClient.contractAddress}_2`, + batch_id: '0', + amount: '5000', }, 1.6, undefined, @@ -1472,6 +1499,76 @@ describe('Auto withdrawer', () => { parseInt(balance.data.balance.amount, 10) - balanceBefore, ).toBeCloseTo(expectedWithdrawnAmount, -1); + const bondings = await autoWithdrawerContractClient.queryBondings({ + user: neutronUserAddress, + }); + expect(bondings).toEqual({ + bondings: [ + { + bonding_id: `${neutronUserAddress}_0`, + bonder: neutronUserAddress, + deposit: [ + { + amount: '30000', + denom: 'untrn', + }, + ], + withdrawal_amount: '15000', + }, + ], + next_page_key: null, + }); + await checkExchangeRate(context); + }); + it('withdraw full amount', async () => { + const { + neutronUserAddress, + neutronClient, + neutronIBCDenom, + autoWithdrawerContractClient, + } = context; + const expectedWithdrawnAmount = 15000; + const expectedBatchWithdrawnAmount = 20000; + + const balanceBefore = parseInt( + ( + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronUserAddress, + { denom: neutronIBCDenom }, + ) + ).data.balance.amount, + ); + + const res = await autoWithdrawerContractClient.withdraw( + neutronUserAddress, + { + batch_id: '0', + amount: '15000', + }, + 1.6, + undefined, + [], + ); + expect(res.transactionHash).toHaveLength(64); + + const withdrawnBatch = + await context.coreContractClient.queryUnbondBatch({ + batch_id: '0', + }); + expect(parseInt(withdrawnBatch.withdrawn_amount, 10)).toBeCloseTo( + expectedBatchWithdrawnAmount, + -1, + ); + + const balance = + await neutronClient.CosmosBankV1Beta1.query.queryBalance( + neutronUserAddress, + { denom: neutronIBCDenom }, + ); + expect( + parseInt(balance.data.balance.amount, 10) - balanceBefore, + ).toBeCloseTo(expectedWithdrawnAmount, -1); + const bondings = await autoWithdrawerContractClient.queryBondings({ user: neutronUserAddress, }); diff --git a/integration_tests/src/testcases/core-slashing.test.ts b/integration_tests/src/testcases/core-slashing.test.ts index 34aafa8ef..94f026f70 100644 --- a/integration_tests/src/testcases/core-slashing.test.ts +++ b/integration_tests/src/testcases/core-slashing.test.ts @@ -98,6 +98,7 @@ describe('Core Slashing', () => { codeIds: { core?: number; token?: number; + withdrawalToken?: number; withdrawalVoucher?: number; withdrawalManager?: number; strategy?: number; @@ -284,6 +285,17 @@ describe('Core Slashing', () => { expect(res.codeId).toBeGreaterThan(0); context.codeIds.token = res.codeId; } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_withdrawal_token.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.withdrawalToken = res.codeId; + } { const res = await client.upload( account.address, @@ -407,6 +419,7 @@ describe('Core Slashing', () => { code_ids: { core_code_id: context.codeIds.core, token_code_id: context.codeIds.token, + withdrawal_token_code_id: context.codeIds.withdrawalToken, withdrawal_voucher_code_id: context.codeIds.withdrawalVoucher, withdrawal_manager_code_id: context.codeIds.withdrawalManager, strategy_code_id: context.codeIds.strategy, diff --git a/integration_tests/src/testcases/core.test.ts b/integration_tests/src/testcases/core.test.ts index 7eec14b24..662ac09ba 100644 --- a/integration_tests/src/testcases/core.test.ts +++ b/integration_tests/src/testcases/core.test.ts @@ -6,6 +6,7 @@ import { DropPuppeteer, DropStrategy, DropWithdrawalManager, + DropWithdrawalToken, DropWithdrawalVoucher, DropRewardsManager, DropStaker, @@ -50,6 +51,7 @@ const DropPumpClass = DropPump.Client; const DropStakerClass = DropStaker.Client; const DropPuppeteerClass = DropPuppeteer.Client; const DropStrategyClass = DropStrategy.Client; +const DropWithdrawalTokenClass = DropWithdrawalToken.Client; const DropWithdrawalVoucherClass = DropWithdrawalVoucher.Client; const DropWithdrawalManagerClass = DropWithdrawalManager.Client; const DropRewardsManagerClass = DropRewardsManager.Client; @@ -74,6 +76,9 @@ describe('Core', () => { puppeteerContractClient?: InstanceType; splitterContractClient?: InstanceType; tokenContractClient?: InstanceType; + withdrawalTokenContractClient?: InstanceType< + typeof DropWithdrawalTokenClass + >; withdrawalVoucherContractClient?: InstanceType< typeof DropWithdrawalVoucherClass >; @@ -102,6 +107,7 @@ describe('Core', () => { codeIds: { core?: number; token?: number; + withdrawalToken?: number; withdrawalVoucher?: number; withdrawalManager?: number; redemptionRateAdapter?: number; @@ -291,6 +297,17 @@ describe('Core', () => { expect(res.codeId).toBeGreaterThan(0); context.codeIds.token = res.codeId; } + { + const res = await client.upload( + account.address, + fs.readFileSync( + join(__dirname, '../../../artifacts/drop_withdrawal_token.wasm'), + ), + 1.5, + ); + expect(res.codeId).toBeGreaterThan(0); + context.codeIds.withdrawalToken = res.codeId; + } { const res = await client.upload( account.address, @@ -428,6 +445,7 @@ describe('Core', () => { code_ids: { core_code_id: context.codeIds.core, token_code_id: context.codeIds.token, + withdrawal_token_code_id: context.codeIds.withdrawalToken, withdrawal_voucher_code_id: context.codeIds.withdrawalVoucher, withdrawal_manager_code_id: context.codeIds.withdrawalManager, strategy_code_id: context.codeIds.strategy, @@ -515,6 +533,13 @@ describe('Core', () => { res.core_contract, ); expect(coreContractInfo.data.contract_info.label).toBe('drop-staking-core'); + const withdrawalTokenContractInfo = + await neutronClient.CosmwasmWasmV1.query.queryContractInfo( + res.withdrawal_token_contract, + ); + expect(withdrawalTokenContractInfo.data.contract_info.label).toBe( + 'drop-staking-withdrawal-token', + ); const withdrawalVoucherContractInfo = await neutronClient.CosmwasmWasmV1.query.queryContractInfo( res.withdrawal_voucher_contract, @@ -539,6 +564,10 @@ describe('Core', () => { context.coreContractClient = instrumentCoreClass( new DropCore.Client(context.client, res.core_contract), ); + context.withdrawalTokenContractClient = new DropWithdrawalToken.Client( + context.client, + res.withdrawal_token_contract, + ); context.withdrawalVoucherContractClient = new DropWithdrawalVoucher.Client( context.client, res.withdrawal_voucher_contract, @@ -2088,66 +2117,22 @@ describe('Core', () => { }); describe('forth cycle', () => { - it('validate NFT', async () => { - const { withdrawalVoucherContractClient, neutronUserAddress } = context; - const vouchers = await withdrawalVoucherContractClient.queryTokens({ - owner: context.neutronUserAddress, - }); - expect(vouchers.tokens.length).toBe(2); - expect(vouchers.tokens[0]).toBe(`0_${neutronUserAddress}_1`); - let tokenId = vouchers.tokens[0]; - let voucher = await withdrawalVoucherContractClient.queryNftInfo({ - token_id: tokenId, - }); - expect(voucher).toBeTruthy(); - expect(voucher).toMatchObject({ - extension: { - amount: '200000', - attributes: [ - { - display_type: null, - trait_type: 'unbond_batch_id', - value: '0', - }, - { - display_type: null, - trait_type: 'received_amount', - value: '200000', - }, - ], - batch_id: '0', - description: 'Withdrawal voucher', - name: 'LDV voucher', - }, - token_uri: null, - }); - expect(vouchers.tokens[1]).toBe(`0_${neutronUserAddress}_2`); - tokenId = vouchers.tokens[1]; - voucher = await withdrawalVoucherContractClient.queryNftInfo({ - token_id: tokenId, - }); - expect(voucher).toBeTruthy(); - expect(voucher).toMatchObject({ - extension: { - amount: '300000', - attributes: [ - { - display_type: null, - trait_type: 'unbond_batch_id', - value: '0', - }, - { - display_type: null, - trait_type: 'received_amount', - value: '300000', - }, - ], - batch_id: '0', - description: 'Withdrawal voucher', - name: 'LDV voucher', - }, - token_uri: null, - }); + it('validates withdrawal tokens balance', async () => { + const { + neutronClient, + neutronUserAddress, + withdrawalTokenContractClient, + } = context; + const balances = + await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( + neutronUserAddress, + ); + const withdrawalTokenBalance = balances.data.balances.find( + (b) => + b.denom === + `factory/${withdrawalTokenContractClient.contractAddress}/drop:unbond:0`, + ); + expect(withdrawalTokenBalance.amount).eq('500000'); }); it('bond tokenized share from registered validator', async () => { const { coreContractClient, neutronUserAddress } = context; @@ -2168,7 +2153,8 @@ describe('Core', () => { }); it('try to withdraw from paused manager', async () => { const { - withdrawalVoucherContractClient, + withdrawalManagerContractClient, + withdrawalTokenContractClient, neutronUserAddress, factoryContractClient: contractClient, account, @@ -2176,34 +2162,42 @@ describe('Core', () => { await contractClient.pause(account.address); - const tokenId = `0_${neutronUserAddress}_1`; await expect( - withdrawalVoucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }), + withdrawalManagerContractClient.receiveWithdrawalDenoms( + neutronUserAddress, + {}, + 'auto', + null, + [ + { + denom: `factory/${withdrawalTokenContractClient.contractAddress}/drop:unbond:0`, + amount: '100000', + }, + ], + ), ).rejects.toThrowError(/Contract execution is paused/); await contractClient.unpause(account.address); }); it('try to withdraw before withdrawn', async () => { - const { withdrawalVoucherContractClient, neutronUserAddress } = context; - const tokenId = `0_${neutronUserAddress}_1`; + const { + withdrawalManagerContractClient, + withdrawalTokenContractClient, + neutronUserAddress, + } = context; await expect( - withdrawalVoucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }), + withdrawalManagerContractClient.receiveWithdrawalDenoms( + neutronUserAddress, + {}, + 'auto', + null, + [ + { + denom: `factory/${withdrawalTokenContractClient.contractAddress}/drop:unbond:0`, + amount: '100000', + }, + ], + ), ).rejects.toThrowError(/is not withdrawn yet/); }); it('update idle interval', async () => { @@ -2344,20 +2338,23 @@ describe('Core', () => { }); it('withdraw with non funded withdrawal manager', async () => { const { - withdrawalVoucherContractClient: voucherContractClient, + withdrawalTokenContractClient, + withdrawalManagerContractClient, neutronUserAddress, } = context; - const tokenId = `0_${neutronUserAddress}_1`; await expect( - voucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }), + withdrawalManagerContractClient.receiveWithdrawalDenoms( + neutronUserAddress, + {}, + 'auto', + null, + [ + { + denom: `factory/${withdrawalTokenContractClient.contractAddress}/drop:unbond:0`, + amount: '100000', + }, + ], + ), ).rejects.toThrowError(/spendable balance [\w/]+ is smaller than/); }); it('fund withdrawal manager', async () => { @@ -2382,7 +2379,8 @@ describe('Core', () => { }); it('withdraw', async () => { const { - withdrawalVoucherContractClient: voucherContractClient, + withdrawalTokenContractClient, + withdrawalManagerContractClient, neutronUserAddress, neutronClient, neutronIBCDenom, @@ -2395,16 +2393,19 @@ describe('Core', () => { ) ).data.balance.amount, ); - const tokenId = `0_${neutronUserAddress}_1`; - const res = await voucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: {}, - }), - ).toString('base64'), - }); + const res = + await withdrawalManagerContractClient.receiveWithdrawalDenoms( + neutronUserAddress, + {}, + 'auto', + null, + [ + { + denom: `factory/${withdrawalTokenContractClient.contractAddress}/drop:unbond:0`, + amount: '200000', + }, + ], + ); expect(res.transactionHash).toHaveLength(64); const balance = await neutronClient.CosmosBankV1Beta1.query.queryBalance( @@ -2418,7 +2419,8 @@ describe('Core', () => { }); it('withdraw to custom receiver', async () => { const { - withdrawalVoucherContractClient: voucherContractClient, + withdrawalTokenContractClient, + withdrawalManagerContractClient, neutronUserAddress, neutronSecondUserAddress, neutronClient, @@ -2433,18 +2435,19 @@ describe('Core', () => { ).data.balance.amount, ); expect(balanceBefore).toEqual(0); - const tokenId = `0_${neutronUserAddress}_2`; - const res = await voucherContractClient.sendNft(neutronUserAddress, { - token_id: tokenId, - contract: context.withdrawalManagerContractClient.contractAddress, - msg: Buffer.from( - JSON.stringify({ - withdraw: { - receiver: neutronSecondUserAddress, + const res = + await withdrawalManagerContractClient.receiveWithdrawalDenoms( + neutronUserAddress, + { receiver: neutronSecondUserAddress }, + 'auto', + null, + [ + { + denom: `factory/${withdrawalTokenContractClient.contractAddress}/drop:unbond:0`, + amount: '300000', }, - }), - ).toString('base64'), - }); + ], + ); expect(res.transactionHash).toHaveLength(64); const balance = await neutronClient.CosmosBankV1Beta1.query.queryBalance( diff --git a/integration_tests/src/tests.yml b/integration_tests/src/tests.yml index 8962fc5ff..2637ca9a5 100644 --- a/integration_tests/src/tests.yml +++ b/integration_tests/src/tests.yml @@ -34,7 +34,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev - uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: nightly-2023-12-21 profile: minimal override: true - name: Install cargo-tarpaulin diff --git a/packages/base/src/error/mod.rs b/packages/base/src/error/mod.rs index 753a7270a..5c04dbb36 100644 --- a/packages/base/src/error/mod.rs +++ b/packages/base/src/error/mod.rs @@ -6,3 +6,5 @@ pub mod redemption_rate_adapter; pub mod rewards_manager; pub mod splitter; pub mod validatorset; +pub mod withdrawal_exchange; +pub mod withdrawal_token; diff --git a/packages/base/src/error/withdrawal_exchange.rs b/packages/base/src/error/withdrawal_exchange.rs new file mode 100644 index 000000000..3a0b7d934 --- /dev/null +++ b/packages/base/src/error/withdrawal_exchange.rs @@ -0,0 +1,30 @@ +use cosmwasm_std::StdError; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + NeutronError(#[from] neutron_sdk::NeutronError), + + #[error("{0}")] + PaymentError(#[from] cw_utils::PaymentError), + + #[error("{0}")] + OwnershipError(#[from] cw_ownable::OwnershipError), + + #[error("unauthorized")] + Unauthorized, + + #[error("Semver parsing error: {0}")] + SemVer(String), +} + +impl From for ContractError { + fn from(err: semver::Error) -> Self { + Self::SemVer(err.to_string()) + } +} + +pub type ContractResult = Result; diff --git a/packages/base/src/error/withdrawal_token.rs b/packages/base/src/error/withdrawal_token.rs new file mode 100644 index 000000000..db1792e7e --- /dev/null +++ b/packages/base/src/error/withdrawal_token.rs @@ -0,0 +1,36 @@ +use cosmwasm_std::StdError; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + NeutronError(#[from] neutron_sdk::NeutronError), + + #[error("{0}")] + PaymentError(#[from] cw_utils::PaymentError), + + #[error("{0}")] + OwnershipError(#[from] cw_ownable::OwnershipError), + + #[error("unauthorized")] + Unauthorized, + + #[error("nothing to mint")] + NothingToMint, + + #[error("unknown reply id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Semver parsing error: {0}")] + SemVer(String), +} + +impl From for ContractError { + fn from(err: semver::Error) -> Self { + Self::SemVer(err.to_string()) + } +} + +pub type ContractResult = Result; diff --git a/packages/base/src/msg/core.rs b/packages/base/src/msg/core.rs index ad14b3478..67ca1f8e5 100644 --- a/packages/base/src/msg/core.rs +++ b/packages/base/src/msg/core.rs @@ -17,6 +17,7 @@ pub struct InstantiateMsg { pub puppeteer_contract: String, pub strategy_contract: String, pub staker_contract: String, + pub withdrawal_token_contract: String, pub withdrawal_voucher_contract: String, pub withdrawal_manager_contract: String, pub validators_set_contract: String, @@ -45,6 +46,7 @@ impl InstantiateMsg { puppeteer_contract: deps.api.addr_validate(&self.puppeteer_contract)?, strategy_contract: deps.api.addr_validate(&self.strategy_contract)?, staker_contract: deps.api.addr_validate(&self.staker_contract)?, + withdrawal_token_contract: deps.api.addr_validate(&self.withdrawal_token_contract)?, withdrawal_voucher_contract: deps .api .addr_validate(&self.withdrawal_voucher_contract)?, diff --git a/packages/base/src/msg/mod.rs b/packages/base/src/msg/mod.rs index eddf5ff25..6c6571f9b 100644 --- a/packages/base/src/msg/mod.rs +++ b/packages/base/src/msg/mod.rs @@ -18,5 +18,7 @@ mod tests; pub mod token; pub mod validatorset; pub mod validatorsstats; +pub mod withdrawal_exchange; pub mod withdrawal_manager; +pub mod withdrawal_token; pub mod withdrawal_voucher; diff --git a/packages/base/src/msg/withdrawal_exchange.rs b/packages/base/src/msg/withdrawal_exchange.rs new file mode 100644 index 000000000..adf1c6e20 --- /dev/null +++ b/packages/base/src/msg/withdrawal_exchange.rs @@ -0,0 +1,29 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; + +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ConfigResponse)] + Config {}, +} + +#[cw_serde] +pub struct ConfigResponse { + pub withdrawal_token_address: String, +} + +#[cw_ownable_execute] +#[cw_serde] +pub enum ExecuteMsg { + Exchange {}, +} +#[cw_serde] +pub struct InstantiateMsg { + pub withdrawal_token_address: String, + pub owner: String, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/base/src/msg/withdrawal_manager.rs b/packages/base/src/msg/withdrawal_manager.rs index 25d7199fe..02ea9b041 100644 --- a/packages/base/src/msg/withdrawal_manager.rs +++ b/packages/base/src/msg/withdrawal_manager.rs @@ -8,6 +8,7 @@ use drop_macros::{pausable, pausable_query}; #[cw_serde] pub struct InstantiateMsg { pub core_contract: String, + pub token_contract: String, pub voucher_contract: String, pub base_denom: String, pub owner: String, @@ -32,6 +33,9 @@ pub enum ExecuteMsg { base_denom: Option, }, ReceiveNft(Cw721ReceiveMsg), + ReceiveWithdrawalDenoms { + receiver: Option, + }, } #[cw_serde] diff --git a/packages/base/src/msg/withdrawal_token.rs b/packages/base/src/msg/withdrawal_token.rs new file mode 100644 index 000000000..d543e9c02 --- /dev/null +++ b/packages/base/src/msg/withdrawal_token.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; + +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ConfigResponse)] + Config {}, +} + +#[cw_serde] +pub struct ConfigResponse { + pub core_address: String, + pub withdrawal_manager_address: String, + pub denom_prefix: String, +} + +#[cw_ownable_execute] +#[cw_serde] +pub enum ExecuteMsg { + CreateDenom { + batch_id: Uint128, + }, + Mint { + amount: Uint128, + receiver: String, + batch_id: Uint128, + }, + Burn { + batch_id: Uint128, + }, +} +#[cw_serde] +pub struct InstantiateMsg { + pub core_address: String, + pub withdrawal_manager_address: String, + pub denom_prefix: String, + pub owner: String, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index d3c4727ff..be376719c 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -11,6 +11,7 @@ pub struct ConfigOptional { pub puppeteer_contract: Option, pub strategy_contract: Option, pub staker_contract: Option, + pub withdrawal_token_contract: Option, pub withdrawal_voucher_contract: Option, pub withdrawal_manager_contract: Option, pub validators_set_contract: Option, @@ -37,6 +38,7 @@ pub struct Config { pub puppeteer_contract: Addr, pub strategy_contract: Addr, pub staker_contract: Addr, + pub withdrawal_token_contract: Addr, pub withdrawal_voucher_contract: Addr, pub withdrawal_manager_contract: Addr, pub validators_set_contract: Addr, diff --git a/packages/base/src/state/mod.rs b/packages/base/src/state/mod.rs index 8944f0970..bacfb98f7 100644 --- a/packages/base/src/state/mod.rs +++ b/packages/base/src/state/mod.rs @@ -14,5 +14,7 @@ pub mod strategy; pub mod token; pub mod validatorset; pub mod validatorsstats; +pub mod withdrawal_exchange; pub mod withdrawal_manager; +pub mod withdrawal_token; pub mod withdrawal_voucher; diff --git a/packages/base/src/state/withdrawal_exchange.rs b/packages/base/src/state/withdrawal_exchange.rs new file mode 100644 index 000000000..5851246e4 --- /dev/null +++ b/packages/base/src/state/withdrawal_exchange.rs @@ -0,0 +1,4 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +pub const WITHDRAWAL_TOKEN_ADDRESS: Item = Item::new("withdrawal_token"); diff --git a/packages/base/src/state/withdrawal_manager.rs b/packages/base/src/state/withdrawal_manager.rs index 48c501529..3b5e63a95 100644 --- a/packages/base/src/state/withdrawal_manager.rs +++ b/packages/base/src/state/withdrawal_manager.rs @@ -5,6 +5,7 @@ use cw_storage_plus::Item; #[cw_serde] pub struct Config { pub core_contract: Addr, + pub withdrawal_token_contract: Addr, pub withdrawal_voucher_contract: Addr, pub base_denom: String, } diff --git a/packages/base/src/state/withdrawal_token.rs b/packages/base/src/state/withdrawal_token.rs new file mode 100644 index 000000000..eb30df415 --- /dev/null +++ b/packages/base/src/state/withdrawal_token.rs @@ -0,0 +1,6 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +pub const CORE_ADDRESS: Item = Item::new("core"); +pub const WITHDRAWAL_MANAGER_ADDRESS: Item = Item::new("withdrawal_manager"); +pub const DENOM_PREFIX: Item = Item::new("denom_prefix"); diff --git a/scripts/deploy_scripts/.env.instantiate.example b/scripts/deploy_scripts/.env.instantiate.example index 92fa31528..d344c66c5 100644 --- a/scripts/deploy_scripts/.env.instantiate.example +++ b/scripts/deploy_scripts/.env.instantiate.example @@ -50,6 +50,7 @@ validators_set_code_id= puppeteer_code_id= rewards_manager_code_id= strategy_code_id= +withdrawal_token_code_id= withdrawal_manager_code_id= withdrawal_voucher_code_id= pump_code_id= diff --git a/scripts/deploy_scripts/generate_monitoring_config.bash b/scripts/deploy_scripts/generate_monitoring_config.bash index 2607aa573..5cdb4b212 100755 --- a/scripts/deploy_scripts/generate_monitoring_config.bash +++ b/scripts/deploy_scripts/generate_monitoring_config.bash @@ -19,6 +19,7 @@ main() { puppeteer_contract="$(neutrond query wasm contract-state smart "$FACTORY_ADDRESS" '{"state":{}}' "${nq[@]}" | jq -r '.data.puppeteer_contract')" && echo -n '.' staker_contract="$(neutrond query wasm contract-state smart "$FACTORY_ADDRESS" '{"state":{}}' "${nq[@]}" | jq -r '.data.staker_contract')" && echo -n '.' token_contract="$(neutrond query wasm contract-state smart "$FACTORY_ADDRESS" '{"state":{}}' "${nq[@]}" | jq -r '.data.token_contract')" && echo -n '.' + withdrawal_token_contract="$(neutrond query wasm contract-state smart "$FACTORY_ADDRESS" '{"state":{}}' "${nq[@]}" | jq -r '.data.withdrawal_token_contract')" && echo -n '.' admin="$(neutrond query wasm contract "$FACTORY_ADDRESS" "${nq[@]}" | jq -r '.contract_info.admin')" && echo -n '.' base_ibc_denom="$(neutrond query wasm contract-state smart "$core_contract" '{"config":{}}' "${nq[@]}" | jq -r '.data.base_denom')" && echo -n '.' base_denom="$(neutrond query ibc-transfer denom-trace "$base_ibc_denom" "${nq[@]}" | jq -r '.denom_trace.base_denom')" && echo -n '.' @@ -108,30 +109,31 @@ EOF echo echo # shellcheck disable=SC2059 - printf "${config}\n" \ - "$NEUTRON_RPC" \ - "$FACTORY_ADDRESS" \ - "$core_contract" \ - "$puppeteer_contract" \ - "$staker_contract" \ - "$token_contract" \ - "$admin" \ - "$base_ibc_denom" \ - "$admin" \ - "$base_denom" \ - "$core_contract" \ - "$base_ibc_denom" \ - "$puppeteer_contract" \ - "$base_ibc_denom" \ - "$puppeteer_ica" \ - "$base_denom" \ - "$staker_contract" \ - "$base_ibc_denom" \ - "$staker_ica" \ - "$base_denom" \ - "$pump_ica" \ - "$base_denom" \ - "$puppeteer_ica" \ + printf "${config}\n" \ + "$NEUTRON_RPC" \ + "$FACTORY_ADDRESS" \ + "$core_contract" \ + "$puppeteer_contract" \ + "$staker_contract" \ + "$token_contract" \ + "$withdrawal_token_contract" \ + "$admin" \ + "$base_ibc_denom" \ + "$admin" \ + "$base_denom" \ + "$core_contract" \ + "$base_ibc_denom" \ + "$puppeteer_contract" \ + "$base_ibc_denom" \ + "$puppeteer_ica" \ + "$base_denom" \ + "$staker_contract" \ + "$base_ibc_denom" \ + "$staker_ica" \ + "$base_denom" \ + "$pump_ica" \ + "$base_denom" \ + "$puppeteer_ica" \ "$watched_validators" } diff --git a/scripts/deploy_scripts/utils.bash b/scripts/deploy_scripts/utils.bash index 40a547a4b..a625df903 100644 --- a/scripts/deploy_scripts/utils.bash +++ b/scripts/deploy_scripts/utils.bash @@ -76,7 +76,7 @@ store_code() { } deploy_wasm_code() { - for contract in factory core distribution puppeteer rewards_manager strategy token staker validators_set withdrawal_manager withdrawal_voucher pump splitter; do + for contract in factory core distribution puppeteer rewards_manager strategy token staker validators_set withdrawal_token withdrawal_manager withdrawal_voucher pump splitter; do store_code "$contract" code_id="${contract}_code_id" printf '[OK] %-24s code ID: %s\n' "$contract" "${!code_id}" @@ -115,7 +115,7 @@ pre_deploy_check_ibc_connection() { } pre_deploy_check_code_ids() { - for contract in factory core distribution puppeteer rewards_manager strategy token staker validators_set withdrawal_manager withdrawal_voucher pump splitter; do + for contract in factory core distribution puppeteer rewards_manager strategy token staker validators_set withdrawal_token withdrawal_manager withdrawal_voucher pump splitter; do code_id="${contract}_code_id" set +u if [[ -z "${!code_id}" ]]; then @@ -140,6 +140,7 @@ deploy_factory() { "core_code_id":'"$core_code_id"', "token_code_id":'"$token_code_id"', "withdrawal_voucher_code_id":'"$withdrawal_voucher_code_id"', + "withdrawal_token_code_id":'"$withdrawal_token_code_id"', "withdrawal_manager_code_id":'"$withdrawal_manager_code_id"', "strategy_code_id":'"$strategy_code_id"', "distribution_code_id":'"$distribution_code_id"', @@ -205,10 +206,13 @@ deploy_factory() { | jq -r '.data.rewards_pump_contract')" puppeteer_address="$(neutrond query wasm contract-state smart "$factory_address" '{"state":{}}' "${nq[@]}" \ | jq -r '.data.puppeteer_contract')" + withdrawal_token_address="$(neutrond query wasm contract-state smart "$factory_address" '{"state":{}}' "${nq[@]}" \ + | jq -r '.data.withdrawal_token_contract')" withdrawal_manager_address="$(neutrond query wasm contract-state smart "$factory_address" '{"state":{}}' "${nq[@]}" \ | jq -r '.data.withdrawal_manager_contract')" echo "[OK] Staker contract: $staker_address" echo "[OK] Puppeteer contract: $puppeteer_address" + echo "[OK] Withdrawal token contract: $withdrawal_token_address" echo "[OK] Withdrawal manager contract: $withdrawal_manager_address" } diff --git a/ts-client/lib/contractLib/dropAutoWithdrawer.d.ts b/ts-client/lib/contractLib/dropAutoWithdrawer.d.ts index 866a36b3e..2d93cb8b8 100644 --- a/ts-client/lib/contractLib/dropAutoWithdrawer.d.ts +++ b/ts-client/lib/contractLib/dropAutoWithdrawer.d.ts @@ -29,10 +29,20 @@ export type Uint64 = string; export type BondArgs = { with_ld_assets: {}; } | { - with_n_f_t: { - token_id: string; + with_withdrawal_denoms: { + batch_id: Uint128; }; }; +/** + * A human readable address. + * + * In Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length. + * + * This type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances. + * + * This type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance. + */ +export type Addr = string; export interface DropAutoWithdrawerSchema { responses: BondingsResponse | InstantiateMsg; query: BondingsArgs; @@ -46,8 +56,9 @@ export interface BondingsResponse { } export interface BondingResponse { bonder: string; + bonding_id: string; deposit: Coin[]; - token_id: string; + withdrawal_amount: Uint128; } export interface Coin { amount: Uint128; @@ -57,8 +68,9 @@ export interface Coin { export interface InstantiateMsg { core_address: string; ld_token: string; + withdrawal_denom_prefix: string; withdrawal_manager_address: string; - withdrawal_voucher_address: string; + withdrawal_token_address: string; } export interface BondingsArgs { /** @@ -75,16 +87,19 @@ export interface BondingsArgs { user?: string | null; } export interface UnbondArgs { - token_id: string; + batch_id: Uint128; } export interface WithdrawArgs { - token_id: string; + amount: Uint128; + batch_id: Uint128; + receiver?: Addr | null; } export interface InstantiateMsg1 { core_address: string; ld_token: string; + withdrawal_denom_prefix: string; withdrawal_manager_address: string; - withdrawal_voucher_address: string; + withdrawal_token_address: string; } export declare class Client { private readonly client; diff --git a/ts-client/lib/contractLib/dropCore.d.ts b/ts-client/lib/contractLib/dropCore.d.ts index 206e6f695..03da821b9 100644 --- a/ts-client/lib/contractLib/dropCore.d.ts +++ b/ts-client/lib/contractLib/dropCore.d.ts @@ -274,6 +274,7 @@ export interface Config { unbonding_safe_period: number; validators_set_contract: Addr; withdrawal_manager_contract: Addr; + withdrawal_token_contract: Addr; withdrawal_voucher_contract: Addr; } export interface FailedBatchResponse { @@ -430,6 +431,7 @@ export interface ConfigOptional { unbonding_safe_period?: number | null; validators_set_contract?: string | null; withdrawal_manager_contract?: string | null; + withdrawal_token_contract?: string | null; withdrawal_voucher_contract?: string | null; } export interface UpdateWithdrawnAmountArgs { @@ -479,6 +481,7 @@ export interface InstantiateMsg { unbonding_safe_period: number; validators_set_contract: string; withdrawal_manager_contract: string; + withdrawal_token_contract: string; withdrawal_voucher_contract: string; } export declare class Client { diff --git a/ts-client/lib/contractLib/dropFactory.d.ts b/ts-client/lib/contractLib/dropFactory.d.ts index 6ceee001b..3619b1876 100644 --- a/ts-client/lib/contractLib/dropFactory.d.ts +++ b/ts-client/lib/contractLib/dropFactory.d.ts @@ -715,6 +715,7 @@ export interface State { token_contract: string; validators_set_contract: string; withdrawal_manager_contract: string; + withdrawal_token_contract: string; withdrawal_voucher_contract: string; } export interface ConfigOptional { @@ -739,6 +740,7 @@ export interface ConfigOptional { unbonding_safe_period?: number | null; validators_set_contract?: string | null; withdrawal_manager_contract?: string | null; + withdrawal_token_contract?: string | null; withdrawal_voucher_contract?: string | null; } export interface ConfigOptional2 { @@ -1140,6 +1142,7 @@ export interface CodeIds { token_code_id: number; validators_set_code_id: number; withdrawal_manager_code_id: number; + withdrawal_token_code_id: number; withdrawal_voucher_code_id: number; } export interface CoreParams { diff --git a/ts-client/lib/contractLib/dropWithdrawalManager.d.ts b/ts-client/lib/contractLib/dropWithdrawalManager.d.ts index e0ae5e4e9..f558f2be3 100644 --- a/ts-client/lib/contractLib/dropWithdrawalManager.d.ts +++ b/ts-client/lib/contractLib/dropWithdrawalManager.d.ts @@ -64,13 +64,14 @@ export type UpdateOwnershipArgs = { } | "accept_ownership" | "renounce_ownership"; export interface DropWithdrawalManagerSchema { responses: Config | OwnershipForString | PauseInfoResponse; - execute: UpdateConfigArgs | ReceiveNftArgs | UpdateOwnershipArgs; + execute: UpdateConfigArgs | ReceiveNftArgs | ReceiveWithdrawalDenomsArgs | UpdateOwnershipArgs; instantiate?: InstantiateMsg; [k: string]: unknown; } export interface Config { base_denom: string; core_contract: Addr; + withdrawal_token_contract: Addr; withdrawal_voucher_contract: Addr; } /** @@ -107,10 +108,14 @@ export interface ReceiveNftArgs { }; additionalProperties?: never; } +export interface ReceiveWithdrawalDenomsArgs { + receiver?: string | null; +} export interface InstantiateMsg { base_denom: string; core_contract: string; owner: string; + token_contract: string; voucher_contract: string; } export declare class Client { @@ -125,6 +130,7 @@ export declare class Client { queryPauseInfo: () => Promise; updateConfig: (sender: string, args: UpdateConfigArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; receiveNft: (sender: string, args: ReceiveNftArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; + receiveWithdrawalDenoms: (sender: string, args: ReceiveWithdrawalDenomsArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; updateOwnership: (sender: string, args: UpdateOwnershipArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; pause: (sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; unpause: (sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; diff --git a/ts-client/lib/contractLib/dropWithdrawalManager.js b/ts-client/lib/contractLib/dropWithdrawalManager.js index b588d7d25..16dc8d10c 100644 --- a/ts-client/lib/contractLib/dropWithdrawalManager.js +++ b/ts-client/lib/contractLib/dropWithdrawalManager.js @@ -47,6 +47,12 @@ class Client { } return this.client.execute(sender, this.contractAddress, { receive_nft: args }, fee || "auto", memo, funds); }; + receiveWithdrawalDenoms = async (sender, args, fee, memo, funds) => { + if (!isSigningCosmWasmClient(this.client)) { + throw this.mustBeSigningClient(); + } + return this.client.execute(sender, this.contractAddress, { receive_withdrawal_denoms: args }, fee || "auto", memo, funds); + }; updateOwnership = async (sender, args, fee, memo, funds) => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); diff --git a/ts-client/lib/contractLib/dropWithdrawalToken.d.ts b/ts-client/lib/contractLib/dropWithdrawalToken.d.ts new file mode 100644 index 000000000..d62ce8c54 --- /dev/null +++ b/ts-client/lib/contractLib/dropWithdrawalToken.d.ts @@ -0,0 +1,119 @@ +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult, InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import { StdFee } from "@cosmjs/amino"; +import { Coin } from "@cosmjs/amino"; +/** + * Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future) + */ +export type Expiration = { + at_height: number; +} | { + at_time: Timestamp; +} | { + never: {}; +}; +/** + * 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; +/** + * 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; +/** + * Actions that can be taken to alter the contract's ownership + */ +export type UpdateOwnershipArgs = { + transfer_ownership: { + expiry?: Expiration | null; + new_owner: string; + }; +} | "accept_ownership" | "renounce_ownership"; +export interface DropWithdrawalTokenSchema { + responses: ConfigResponse | OwnershipForString; + execute: CreateDenomArgs | MintArgs | BurnArgs | UpdateOwnershipArgs; + instantiate?: InstantiateMsg; + [k: string]: unknown; +} +export interface ConfigResponse { + core_address: string; + denom_prefix: string; + withdrawal_manager_address: string; +} +/** + * The contract's ownership info + */ +export interface OwnershipForString { + /** + * The contract's current owner. `None` if the ownership has been renounced. + */ + owner?: string | null; + /** + * The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline. + */ + pending_expiry?: Expiration | null; + /** + * The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer. + */ + pending_owner?: string | null; +} +export interface CreateDenomArgs { + batch_id: Uint128; +} +export interface MintArgs { + amount: Uint128; + batch_id: Uint128; + receiver: string; +} +export interface BurnArgs { + batch_id: Uint128; +} +export interface InstantiateMsg { + core_address: string; + denom_prefix: string; + owner: string; + withdrawal_manager_address: string; +} +export declare class Client { + private readonly client; + contractAddress: string; + constructor(client: CosmWasmClient | SigningCosmWasmClient, contractAddress: string); + mustBeSigningClient(): Error; + static instantiate(client: SigningCosmWasmClient, sender: string, codeId: number, initMsg: InstantiateMsg, label: string, fees: StdFee | 'auto' | number, initCoins?: readonly Coin[]): Promise; + static instantiate2(client: SigningCosmWasmClient, sender: string, codeId: number, salt: number, initMsg: InstantiateMsg, label: string, fees: StdFee | 'auto' | number, initCoins?: readonly Coin[]): Promise; + queryConfig: () => Promise; + queryOwnership: () => Promise; + createDenom: (sender: string, args: CreateDenomArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; + mint: (sender: string, args: MintArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; + burn: (sender: string, args: BurnArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; + updateOwnership: (sender: string, args: UpdateOwnershipArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise; +} diff --git a/ts-client/lib/contractLib/dropWithdrawalToken.js b/ts-client/lib/contractLib/dropWithdrawalToken.js new file mode 100644 index 000000000..fa28d3a4d --- /dev/null +++ b/ts-client/lib/contractLib/dropWithdrawalToken.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Client = void 0; +function isSigningCosmWasmClient(client) { + return 'execute' in client; +} +class Client { + client; + contractAddress; + constructor(client, contractAddress) { + this.client = client; + this.contractAddress = contractAddress; + } + mustBeSigningClient() { + return new Error("This client is not a SigningCosmWasmClient"); + } + static async instantiate(client, sender, codeId, initMsg, label, fees, initCoins) { + const res = await client.instantiate(sender, codeId, initMsg, label, fees, { + ...(initCoins && initCoins.length && { funds: initCoins }), + }); + return res; + } + static async instantiate2(client, sender, codeId, salt, initMsg, label, fees, initCoins) { + const res = await client.instantiate2(sender, codeId, new Uint8Array([salt]), initMsg, label, fees, { + ...(initCoins && initCoins.length && { funds: initCoins }), + }); + return res; + } + queryConfig = async () => { + return this.client.queryContractSmart(this.contractAddress, { config: {} }); + }; + queryOwnership = async () => { + return this.client.queryContractSmart(this.contractAddress, { ownership: {} }); + }; + createDenom = async (sender, args, fee, memo, funds) => { + if (!isSigningCosmWasmClient(this.client)) { + throw this.mustBeSigningClient(); + } + return this.client.execute(sender, this.contractAddress, { create_denom: args }, fee || "auto", memo, funds); + }; + mint = async (sender, args, fee, memo, funds) => { + if (!isSigningCosmWasmClient(this.client)) { + throw this.mustBeSigningClient(); + } + return this.client.execute(sender, this.contractAddress, { mint: args }, fee || "auto", memo, funds); + }; + burn = async (sender, args, fee, memo, funds) => { + if (!isSigningCosmWasmClient(this.client)) { + throw this.mustBeSigningClient(); + } + return this.client.execute(sender, this.contractAddress, { burn: args }, fee || "auto", memo, funds); + }; + updateOwnership = async (sender, args, fee, memo, funds) => { + if (!isSigningCosmWasmClient(this.client)) { + throw this.mustBeSigningClient(); + } + return this.client.execute(sender, this.contractAddress, { update_ownership: args }, fee || "auto", memo, funds); + }; +} +exports.Client = Client; diff --git a/ts-client/lib/contractLib/index.d.ts b/ts-client/lib/contractLib/index.d.ts index 974ba8039..221fc4ccc 100644 --- a/ts-client/lib/contractLib/index.d.ts +++ b/ts-client/lib/contractLib/index.d.ts @@ -38,5 +38,7 @@ import * as _18 from './dropValidatorsStats'; export declare const DropValidatorsStats: typeof _18; import * as _19 from './dropWithdrawalManager'; export declare const DropWithdrawalManager: typeof _19; -import * as _20 from './dropWithdrawalVoucher'; -export declare const DropWithdrawalVoucher: typeof _20; +import * as _20 from './dropWithdrawalToken'; +export declare const DropWithdrawalToken: typeof _20; +import * as _21 from './dropWithdrawalVoucher'; +export declare const DropWithdrawalVoucher: typeof _21; diff --git a/ts-client/lib/contractLib/index.js b/ts-client/lib/contractLib/index.js index 0d63217e9..c624ac1b5 100644 --- a/ts-client/lib/contractLib/index.js +++ b/ts-client/lib/contractLib/index.js @@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.DropWithdrawalVoucher = exports.DropWithdrawalManager = exports.DropValidatorsStats = exports.DropValidatorsSet = exports.DropToken = exports.DropStrategy = exports.DropStaker = exports.DropSplitter = exports.DropRewardsManager = exports.DropRedemptionRateAdapter = exports.DropPuppeteer = exports.DropPump = exports.DropProviderProposalsPoc = exports.DropProposalVotesPoc = exports.DropPriceProvider = exports.DropHookTester = exports.DropFactory = exports.DropDistribution = exports.DropCore = exports.DropAutoWithdrawer = exports.DropAstroportExchangeHandler = void 0; +exports.DropWithdrawalVoucher = exports.DropWithdrawalToken = exports.DropWithdrawalManager = exports.DropValidatorsStats = exports.DropValidatorsSet = exports.DropToken = exports.DropStrategy = exports.DropStaker = exports.DropSplitter = exports.DropRewardsManager = exports.DropRedemptionRateAdapter = exports.DropPuppeteer = exports.DropPump = exports.DropProviderProposalsPoc = exports.DropProposalVotesPoc = exports.DropPriceProvider = exports.DropHookTester = exports.DropFactory = exports.DropDistribution = exports.DropCore = exports.DropAutoWithdrawer = exports.DropAstroportExchangeHandler = void 0; const _0 = __importStar(require("./dropAstroportExchangeHandler")); exports.DropAstroportExchangeHandler = _0; const _1 = __importStar(require("./dropAutoWithdrawer")); @@ -64,5 +64,7 @@ const _18 = __importStar(require("./dropValidatorsStats")); exports.DropValidatorsStats = _18; const _19 = __importStar(require("./dropWithdrawalManager")); exports.DropWithdrawalManager = _19; -const _20 = __importStar(require("./dropWithdrawalVoucher")); -exports.DropWithdrawalVoucher = _20; +const _20 = __importStar(require("./dropWithdrawalToken")); +exports.DropWithdrawalToken = _20; +const _21 = __importStar(require("./dropWithdrawalVoucher")); +exports.DropWithdrawalVoucher = _21; diff --git a/ts-client/src/contractLib/dropAutoWithdrawer.ts b/ts-client/src/contractLib/dropAutoWithdrawer.ts index 67af0672d..1b28a2b0f 100644 --- a/ts-client/src/contractLib/dropAutoWithdrawer.ts +++ b/ts-client/src/contractLib/dropAutoWithdrawer.ts @@ -31,10 +31,20 @@ export type BondArgs = with_ld_assets: {}; } | { - with_n_f_t: { - token_id: string; + with_withdrawal_denoms: { + batch_id: Uint128; }; }; +/** + * A human readable address. + * + * In Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length. + * + * This type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances. + * + * This type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance. + */ +export type Addr = string; export interface DropAutoWithdrawerSchema { responses: BondingsResponse | InstantiateMsg; @@ -49,8 +59,9 @@ export interface BondingsResponse { } export interface BondingResponse { bonder: string; + bonding_id: string; deposit: Coin[]; - token_id: string; + withdrawal_amount: Uint128; } export interface Coin { amount: Uint128; @@ -60,8 +71,9 @@ export interface Coin { export interface InstantiateMsg { core_address: string; ld_token: string; + withdrawal_denom_prefix: string; withdrawal_manager_address: string; - withdrawal_voucher_address: string; + withdrawal_token_address: string; } export interface BondingsArgs { /** @@ -78,16 +90,19 @@ export interface BondingsArgs { user?: string | null; } export interface UnbondArgs { - token_id: string; + batch_id: Uint128; } export interface WithdrawArgs { - token_id: string; + amount: Uint128; + batch_id: Uint128; + receiver?: Addr | null; } export interface InstantiateMsg1 { core_address: string; ld_token: string; + withdrawal_denom_prefix: string; withdrawal_manager_address: string; - withdrawal_voucher_address: string; + withdrawal_token_address: string; } diff --git a/ts-client/src/contractLib/dropCore.ts b/ts-client/src/contractLib/dropCore.ts index 02a69c2cf..12c8fb4e2 100644 --- a/ts-client/src/contractLib/dropCore.ts +++ b/ts-client/src/contractLib/dropCore.ts @@ -342,6 +342,7 @@ export interface Config { unbonding_safe_period: number; validators_set_contract: Addr; withdrawal_manager_contract: Addr; + withdrawal_token_contract: Addr; withdrawal_voucher_contract: Addr; } export interface FailedBatchResponse { @@ -494,6 +495,7 @@ export interface ConfigOptional { unbonding_safe_period?: number | null; validators_set_contract?: string | null; withdrawal_manager_contract?: string | null; + withdrawal_token_contract?: string | null; withdrawal_voucher_contract?: string | null; } export interface UpdateWithdrawnAmountArgs { @@ -543,6 +545,7 @@ export interface InstantiateMsg { unbonding_safe_period: number; validators_set_contract: string; withdrawal_manager_contract: string; + withdrawal_token_contract: string; withdrawal_voucher_contract: string; } diff --git a/ts-client/src/contractLib/dropFactory.ts b/ts-client/src/contractLib/dropFactory.ts index 2793fa2f3..a2b29034f 100644 --- a/ts-client/src/contractLib/dropFactory.ts +++ b/ts-client/src/contractLib/dropFactory.ts @@ -789,6 +789,7 @@ export interface State { token_contract: string; validators_set_contract: string; withdrawal_manager_contract: string; + withdrawal_token_contract: string; withdrawal_voucher_contract: string; } export interface ConfigOptional { @@ -813,6 +814,7 @@ export interface ConfigOptional { unbonding_safe_period?: number | null; validators_set_contract?: string | null; withdrawal_manager_contract?: string | null; + withdrawal_token_contract?: string | null; withdrawal_voucher_contract?: string | null; } export interface ConfigOptional2 { @@ -1214,6 +1216,7 @@ export interface CodeIds { token_code_id: number; validators_set_code_id: number; withdrawal_manager_code_id: number; + withdrawal_token_code_id: number; withdrawal_voucher_code_id: number; } export interface CoreParams { diff --git a/ts-client/src/contractLib/dropWithdrawalManager.ts b/ts-client/src/contractLib/dropWithdrawalManager.ts index 2f94f1803..a45054d2b 100644 --- a/ts-client/src/contractLib/dropWithdrawalManager.ts +++ b/ts-client/src/contractLib/dropWithdrawalManager.ts @@ -73,13 +73,14 @@ export type UpdateOwnershipArgs = export interface DropWithdrawalManagerSchema { responses: Config | OwnershipForString | PauseInfoResponse; - execute: UpdateConfigArgs | ReceiveNftArgs | UpdateOwnershipArgs; + execute: UpdateConfigArgs | ReceiveNftArgs | ReceiveWithdrawalDenomsArgs | UpdateOwnershipArgs; instantiate?: InstantiateMsg; [k: string]: unknown; } export interface Config { base_denom: string; core_contract: Addr; + withdrawal_token_contract: Addr; withdrawal_voucher_contract: Addr; } /** @@ -116,10 +117,14 @@ export interface ReceiveNftArgs { }; additionalProperties?: never; } +export interface ReceiveWithdrawalDenomsArgs { + receiver?: string | null; +} export interface InstantiateMsg { base_denom: string; core_contract: string; owner: string; + token_contract: string; voucher_contract: string; } @@ -186,6 +191,10 @@ export class Client { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { receive_nft: args }, fee || "auto", memo, funds); } + receiveWithdrawalDenoms = async(sender:string, args: ReceiveWithdrawalDenomsArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { receive_withdrawal_denoms: args }, fee || "auto", memo, funds); + } updateOwnership = async(sender:string, args: UpdateOwnershipArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { update_ownership: args }, fee || "auto", memo, funds); diff --git a/ts-client/src/contractLib/dropWithdrawalToken.ts b/ts-client/src/contractLib/dropWithdrawalToken.ts new file mode 100644 index 000000000..967298fc0 --- /dev/null +++ b/ts-client/src/contractLib/dropWithdrawalToken.ts @@ -0,0 +1,182 @@ +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult, InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import { StdFee } from "@cosmjs/amino"; +import { Coin } from "@cosmjs/amino"; +/** + * Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future) + */ +export type Expiration = + | { + at_height: number; + } + | { + at_time: Timestamp; + } + | { + never: {}; + }; +/** + * 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; +/** + * 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; +/** + * Actions that can be taken to alter the contract's ownership + */ +export type UpdateOwnershipArgs = + | { + transfer_ownership: { + expiry?: Expiration | null; + new_owner: string; + }; + } + | "accept_ownership" + | "renounce_ownership"; + +export interface DropWithdrawalTokenSchema { + responses: ConfigResponse | OwnershipForString; + execute: CreateDenomArgs | MintArgs | BurnArgs | UpdateOwnershipArgs; + instantiate?: InstantiateMsg; + [k: string]: unknown; +} +export interface ConfigResponse { + core_address: string; + denom_prefix: string; + withdrawal_manager_address: string; +} +/** + * The contract's ownership info + */ +export interface OwnershipForString { + /** + * The contract's current owner. `None` if the ownership has been renounced. + */ + owner?: string | null; + /** + * The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline. + */ + pending_expiry?: Expiration | null; + /** + * The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer. + */ + pending_owner?: string | null; +} +export interface CreateDenomArgs { + batch_id: Uint128; +} +export interface MintArgs { + amount: Uint128; + batch_id: Uint128; + receiver: string; +} +export interface BurnArgs { + batch_id: Uint128; +} +export interface InstantiateMsg { + core_address: string; + denom_prefix: string; + owner: string; + withdrawal_manager_address: string; +} + + +function isSigningCosmWasmClient( + client: CosmWasmClient | SigningCosmWasmClient +): client is SigningCosmWasmClient { + return 'execute' in client; +} + +export class Client { + private readonly client: CosmWasmClient | SigningCosmWasmClient; + contractAddress: string; + constructor(client: CosmWasmClient | SigningCosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + } + mustBeSigningClient() { + return new Error("This client is not a SigningCosmWasmClient"); + } + static async instantiate( + client: SigningCosmWasmClient, + sender: string, + codeId: number, + initMsg: InstantiateMsg, + label: string, + fees: StdFee | 'auto' | number, + initCoins?: readonly Coin[], + ): Promise { + const res = await client.instantiate(sender, codeId, initMsg, label, fees, { + ...(initCoins && initCoins.length && { funds: initCoins }), + }); + return res; + } + static async instantiate2( + client: SigningCosmWasmClient, + sender: string, + codeId: number, + salt: number, + initMsg: InstantiateMsg, + label: string, + fees: StdFee | 'auto' | number, + initCoins?: readonly Coin[], + ): Promise { + const res = await client.instantiate2(sender, codeId, new Uint8Array([salt]), initMsg, label, fees, { + ...(initCoins && initCoins.length && { funds: initCoins }), + }); + return res; + } + queryConfig = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { config: {} }); + } + queryOwnership = async(): Promise => { + return this.client.queryContractSmart(this.contractAddress, { ownership: {} }); + } + createDenom = async(sender:string, args: CreateDenomArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { create_denom: args }, fee || "auto", memo, funds); + } + mint = async(sender:string, args: MintArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { mint: args }, fee || "auto", memo, funds); + } + burn = async(sender:string, args: BurnArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { burn: args }, fee || "auto", memo, funds); + } + updateOwnership = async(sender:string, args: UpdateOwnershipArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { update_ownership: args }, fee || "auto", memo, funds); + } +} diff --git a/ts-client/src/contractLib/index.ts b/ts-client/src/contractLib/index.ts index 68704fbbe..b46232838 100644 --- a/ts-client/src/contractLib/index.ts +++ b/ts-client/src/contractLib/index.ts @@ -58,5 +58,8 @@ export const DropValidatorsStats = _18; import * as _19 from './dropWithdrawalManager'; export const DropWithdrawalManager = _19; -import * as _20 from './dropWithdrawalVoucher'; -export const DropWithdrawalVoucher = _20; +import * as _20 from './dropWithdrawalToken'; +export const DropWithdrawalToken = _20; + +import * as _21 from './dropWithdrawalVoucher'; +export const DropWithdrawalVoucher = _21;