diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index ddf173ca..da171a7f 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -1,8 +1,8 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ attr, ensure, ensure_eq, ensure_ne, to_json_binary, Addr, Attribute, BankMsg, BankQuery, - Binary, Coin, CosmosMsg, CustomQuery, Decimal, Deps, DepsMut, Env, MessageInfo, Order, - QueryRequest, Response, StdError, StdResult, Uint128, Uint64, WasmMsg, + Binary, Coin, CosmosMsg, CustomQuery, Decimal, Decimal256, Deps, DepsMut, Env, MessageInfo, + Order, QueryRequest, Response, StdError, StdResult, Uint128, Uint256, Uint64, WasmMsg, }; use cw_storage_plus::Bound; use drop_helpers::answer::response; @@ -35,6 +35,7 @@ use drop_staking_base::{ }, }; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; +use neutron_sdk::interchain_queries::v047::types::DECIMAL_FRACTIONAL; use prost::Message; pub type MessageWithFeeResponse = (CosmosMsg, Option>); @@ -1005,7 +1006,7 @@ fn execute_bond( r#ref: Option, ) -> ContractResult> { let config = CONFIG.load(deps.storage)?; - let Coin { amount, denom } = cw_utils::one_coin(&info)?; + let Coin { mut amount, denom } = cw_utils::one_coin(&info)?; if let Some(bond_limit) = config.bond_limit { if BONDED_AMOUNT.load(deps.storage)? + amount > bond_limit { return Err(ContractError::BondLimitExceeded {}); @@ -1018,7 +1019,13 @@ fn execute_bond( let exchange_rate = query_exchange_rate(deps.as_ref(), &config)?; attrs.push(attr("exchange_rate", exchange_rate.to_string())); - if let check_denom::DenomType::LsmShare(remote_denom) = denom_type { + if let check_denom::DenomType::LsmShare(remote_denom, validator) = denom_type { + amount = calc_lsm_share_underlying_amount( + deps.as_ref(), + &config.puppeteer_contract, + &amount, + validator, + )?; if amount < config.lsm_min_bond_amount { return Err(ContractError::LSMBondAmountIsBelowMinimum { min_stake_amount: config.lsm_min_bond_amount, @@ -1038,7 +1045,6 @@ fn execute_bond( amount: vec![Coin::new(amount.u128(), denom)], })); } - let issue_amount = amount * (Decimal::one() / exchange_rate); attrs.push(attr("issue_amount", issue_amount.to_string())); @@ -1561,13 +1567,45 @@ fn get_pending_lsm_share_msg( } } +fn calc_lsm_share_underlying_amount( + deps: Deps, + puppeteer_contract: &Addr, + lsm_share: &Uint128, + validator: String, +) -> ContractResult { + let delegations = deps + .querier + .query_wasm_smart::( + puppeteer_contract, + &drop_puppeteer_base::msg::QueryMsg::Extension { + msg: drop_staking_base::msg::puppeteer::QueryExtMsg::Delegations {}, + }, + )? + .delegations + .delegations; + if delegations.is_empty() { + return Err(ContractError::NoDelegations {}); + } + let validator_info = delegations + .iter() + .find(|one| one.validator == validator) + .ok_or(ContractError::ValidatorInfoNotFound { + validator: validator.clone(), + })?; + let share = Decimal256::from_atomics(*lsm_share, 0)?; + Ok(Uint128::try_from( + share.checked_mul(validator_info.share_ratio)?.atomics() + / Uint256::from(DECIMAL_FRACTIONAL), + )?) +} + pub mod check_denom { use super::*; #[derive(PartialEq, Debug)] pub enum DenomType { Base, - LsmShare(String), + LsmShare(String, String), } // XXX: cosmos_sdk_proto defines these structures for me, @@ -1645,7 +1683,10 @@ pub mod check_denom { return Err(ContractError::InvalidDenom {}); } - Ok(DenomType::LsmShare(trace.base_denom)) + Ok(DenomType::LsmShare( + trace.base_denom.to_string(), + validator.to_string(), + )) } } diff --git a/contracts/core/src/tests.rs b/contracts/core/src/tests.rs index 299f1919..933b9563 100644 --- a/contracts/core/src/tests.rs +++ b/contracts/core/src/tests.rs @@ -5,11 +5,14 @@ use crate::contract::{ use cosmwasm_std::{ from_json, testing::{mock_env, mock_info, MockApi, MockStorage}, - to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Event, OwnedDeps, Response, SubMsg, - Timestamp, Uint128, WasmMsg, + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Decimal256, Event, OwnedDeps, + Response, SubMsg, Timestamp, Uint128, WasmMsg, }; use drop_helpers::testing::{mock_dependencies, WasmMockQuerier}; -use drop_puppeteer_base::{msg::TransferReadyBatchesMsg, state::RedeemShareItem}; +use drop_puppeteer_base::{ + msg::TransferReadyBatchesMsg, + state::{Delegations, DropDelegation, RedeemShareItem}, +}; use drop_staking_base::state::core::{FAILED_BATCH_ID, LAST_STAKER_RESPONSE}; use drop_staking_base::{ error::core::ContractError, @@ -28,7 +31,7 @@ use drop_staking_base::{ }; use neutron_sdk::{ bindings::{msg::NeutronMsg, query::NeutronQuery}, - interchain_queries::v045::types::{Balances, Delegations}, + interchain_queries::v045::types::Balances, sudo::msg::RequestPacket, }; use std::vec; @@ -579,13 +582,14 @@ fn test_tick_idle_unbonding_close() { .add_wasm_query_response("puppeteer_contract", |_| { to_json_binary(&DelegationsResponse { delegations: Delegations { - delegations: vec![cosmwasm_std::Delegation { + delegations: vec![DropDelegation { delegator: Addr::unchecked("ica_address"), validator: "valoper_address".to_string(), amount: Coin { denom: "remote_denom".to_string(), amount: Uint128::new(100_000), }, + share_ratio: Decimal256::one(), }], }, remote_height: 10u64, @@ -717,13 +721,14 @@ fn test_tick_idle_claim_wo_unbond() { .add_wasm_query_response("puppeteer_contract", |_| { to_json_binary(&DelegationsResponse { delegations: Delegations { - delegations: vec![cosmwasm_std::Delegation { + delegations: vec![DropDelegation { delegator: Addr::unchecked("ica_address"), validator: "valoper_address".to_string(), amount: Coin { denom: "remote_denom".to_string(), amount: Uint128::new(100_000), }, + share_ratio: Decimal256::one(), }], }, remote_height: 10u64, @@ -874,13 +879,14 @@ fn test_tick_idle_claim_with_unbond_transfer() { .add_wasm_query_response("puppeteer_contract", |_| { to_json_binary(&DelegationsResponse { delegations: Delegations { - delegations: vec![cosmwasm_std::Delegation { + delegations: vec![DropDelegation { delegator: Addr::unchecked("ica_address"), validator: "valoper_address".to_string(), amount: Coin { denom: "remote_denom".to_string(), amount: Uint128::new(100_000), }, + share_ratio: Decimal256::one(), }], }, remote_height: 12344u64, @@ -2948,7 +2954,7 @@ fn test_bond_lsm_share_wrong_channel() { fn test_bond_lsm_share_increase_exchange_rate() { let mut deps = mock_dependencies(&[Coin { denom: "ld_denom".to_string(), - amount: Uint128::new(1), + amount: Uint128::new(1001), }]); deps.querier.add_stargate_query_response( "/ibc.applications.transfer.v1.Query/DenomTrace", @@ -2994,11 +3000,33 @@ fn test_bond_lsm_share_increase_exchange_rate() { .add_wasm_query_response("puppeteer_contract", |_| { to_json_binary(&DelegationsResponse { delegations: Delegations { - delegations: vec![], + delegations: vec![DropDelegation { + delegator: Addr::unchecked("delegator"), + validator: "valoper1".to_string(), + amount: Coin::new(1000, "remote_denom".to_string()), + share_ratio: Decimal256::one(), + }], + }, + remote_height: 10u64, + local_height: 10u64, + timestamp: Timestamp::from_seconds(90001), + }) + .unwrap() + }); + deps.querier + .add_wasm_query_response("puppeteer_contract", |_| { + to_json_binary(&DelegationsResponse { + delegations: Delegations { + delegations: vec![DropDelegation { + delegator: Addr::unchecked("delegator"), + validator: "valoper1".to_string(), + amount: Coin::new(1000, "remote_denom".to_string()), + share_ratio: Decimal256::one(), + }], }, - remote_height: 0, - local_height: 0, - timestamp: Timestamp::from_nanos(1_000_000_202), + remote_height: 10u64, + local_height: 10u64, + timestamp: Timestamp::from_seconds(90001), }) .unwrap() }); @@ -3154,6 +3182,23 @@ fn test_bond_lsm_share_ok() { }) .unwrap() }); + deps.querier + .add_wasm_query_response("puppeteer_contract", |_| { + to_json_binary(&DelegationsResponse { + delegations: Delegations { + delegations: vec![DropDelegation { + delegator: Addr::unchecked("delegator"), + validator: "valoper1".to_string(), + amount: Coin::new(1000, "remote_denom".to_string()), + share_ratio: Decimal256::one(), + }], + }, + remote_height: 10u64, + local_height: 10u64, + timestamp: Timestamp::from_seconds(90001), + }) + .unwrap() + }); let mut env = mock_env(); env.block.time = Timestamp::from_seconds(1000); TOTAL_LSM_SHARES @@ -3746,7 +3791,7 @@ mod check_denom { .unwrap(); assert_eq!( denom_type, - DenomType::LsmShare("valoper12345/1".to_string()) + DenomType::LsmShare("valoper12345/1".to_string(), "valoper12345".to_string()) ); } } diff --git a/contracts/puppeteer/src/contract.rs b/contracts/puppeteer/src/contract.rs index 8e6d4bfa..6b02cf59 100644 --- a/contracts/puppeteer/src/contract.rs +++ b/contracts/puppeteer/src/contract.rs @@ -42,8 +42,8 @@ use drop_puppeteer_base::{ }, proto::MsgIBCTransfer, state::{ - PuppeteerBase, RedeemShareItem, ReplyMsg, TxState, TxStateStatus, UnbondingDelegation, - ICA_ID, LOCAL_DENOM, + Delegations, PuppeteerBase, RedeemShareItem, ReplyMsg, TxState, TxStateStatus, + UnbondingDelegation, ICA_ID, LOCAL_DENOM, }, }; use drop_staking_base::{ @@ -55,8 +55,7 @@ use drop_staking_base::{ use neutron_sdk::{ bindings::{msg::NeutronMsg, query::NeutronQuery, types::ProtobufAny}, interchain_queries::v045::{ - new_register_delegator_unbonding_delegations_query_msg, - types::{Balances, Delegations}, + new_register_delegator_unbonding_delegations_query_msg, types::Balances, }, interchain_txs::helpers::decode_message_response, sudo::msg::{RequestPacket, RequestPacketTimeoutHeight, SudoMsg}, diff --git a/contracts/puppeteer/src/tests.rs b/contracts/puppeteer/src/tests.rs index 0677db99..45b470b9 100644 --- a/contracts/puppeteer/src/tests.rs +++ b/contracts/puppeteer/src/tests.rs @@ -4,7 +4,7 @@ use cosmwasm_schema::schemars; use cosmwasm_std::{ coin, coins, from_json, testing::{mock_env, mock_info}, - to_json_binary, Addr, Binary, CosmosMsg, Delegation, DepsMut, Event, Response, StdError, + to_json_binary, Addr, Binary, CosmosMsg, Decimal256, DepsMut, Event, Response, StdError, SubMsg, Timestamp, Uint128, Uint64, }; use drop_helpers::{ @@ -14,7 +14,8 @@ use drop_helpers::{ testing::mock_dependencies, }; use drop_puppeteer_base::state::{ - BalancesAndDelegations, BalancesAndDelegationsState, PuppeteerBase, ReplyMsg, + BalancesAndDelegations, BalancesAndDelegationsState, Delegations, DropDelegation, + PuppeteerBase, ReplyMsg, }; use drop_staking_base::{ msg::puppeteer::InstantiateMsg, @@ -26,7 +27,7 @@ use neutron_sdk::{ query::{NeutronQuery, QueryRegisteredQueryResultResponse}, types::{InterchainQueryResult, StorageValue}, }, - interchain_queries::v045::types::{Balances, Delegations}, + interchain_queries::v045::types::Balances, query::min_ibc_fee::MinIbcFeeResponse, sudo::msg::SudoMsg, NeutronError, @@ -619,21 +620,23 @@ fn test_sudo_kv_query_result() { }, delegations: Delegations { delegations: vec![ - Delegation { + DropDelegation { delegator: Addr::unchecked( "cosmos1nujy3vl3rww3cy8tf8pdru5jp3f9ppmkadws553ck3qryg2tjanqt39xnv" ), validator: "cosmosvaloper1rndyjagfg0nsedl2uy5n92vssn8aj5n67t0nfx" .to_string(), - amount: coin(13582465152, "stake") + amount: coin(13582465152, "stake"), + share_ratio: Decimal256::one() }, - Delegation { + DropDelegation { delegator: Addr::unchecked( "cosmos1nujy3vl3rww3cy8tf8pdru5jp3f9ppmkadws553ck3qryg2tjanqt39xnv" ), validator: "cosmosvaloper1gh4vzw9wsfgl2h37qqnetet0m4wrzm7v7x3j9x" .to_string(), - amount: coin(13582465152, "stake") + amount: coin(13582465152, "stake"), + share_ratio: Decimal256::one() } ] } diff --git a/contracts/strategy/src/tests.rs b/contracts/strategy/src/tests.rs index 454f2d36..563d06b2 100644 --- a/contracts/strategy/src/tests.rs +++ b/contracts/strategy/src/tests.rs @@ -3,12 +3,13 @@ use crate::contract::instantiate; use cosmwasm_schema::cw_serde; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ - to_json_binary, Addr, Attribute, Binary, Decimal, Deps, Empty, Env, Event, Response, StdResult, - Timestamp, Uint128, + to_json_binary, Addr, Attribute, Binary, Decimal, Decimal256, Deps, Empty, Env, Event, + Response, StdResult, Timestamp, Uint128, }; use cw_multi_test::{custom_app, App, Contract, ContractWrapper, Executor}; use drop_puppeteer_base::error::ContractError as PuppeteerContractError; use drop_puppeteer_base::msg::QueryMsg as PuppeteerQueryMsg; +use drop_puppeteer_base::state::{Delegations, DropDelegation}; use drop_staking_base::error::distribution::ContractError as DistributionContractError; use drop_staking_base::error::validatorset::ContractError as ValidatorSetContractError; use drop_staking_base::msg::strategy::QueryMsg; @@ -16,7 +17,6 @@ use drop_staking_base::msg::validatorset::QueryMsg as ValidatorSetQueryMsg; use drop_staking_base::msg::{ distribution::QueryMsg as DistributionQueryMsg, strategy::InstantiateMsg, }; -use neutron_sdk::interchain_queries::v045::types::Delegations; const CORE_CONTRACT_ADDR: &str = "core_contract"; const PUPPETEER_CONTRACT_ADDR: &str = "puppeteer_contract"; @@ -80,15 +80,16 @@ fn puppeteer_query( PuppeteerQueryMsg::KVQueryIds {} => todo!(), PuppeteerQueryMsg::Extension { msg } => match msg { drop_staking_base::msg::puppeteer::QueryExtMsg::Delegations {} => { - let mut delegations_amount: Vec = Vec::new(); + let mut delegations_amount: Vec = Vec::new(); for i in 0..3 { - let delegation = cosmwasm_std::Delegation { + let delegation = DropDelegation { validator: format!("valoper{}", i), delegator: Addr::unchecked("delegator".to_owned() + i.to_string().as_str()), amount: cosmwasm_std::Coin { denom: "uatom".to_string(), amount: Uint128::from(100u128), }, + share_ratio: Decimal256::one(), }; delegations_amount.push(delegation); } diff --git a/packages/base/src/error/core.rs b/packages/base/src/error/core.rs index 68c1f29e..87aed877 100644 --- a/packages/base/src/error/core.rs +++ b/packages/base/src/error/core.rs @@ -1,4 +1,7 @@ -use cosmwasm_std::{OverflowError, StdError, Uint128}; +use cosmwasm_std::{ + ConversionOverflowError, Decimal256RangeExceeded, DivideByZeroError, OverflowError, StdError, + Uint128, +}; use cw_ownable::OwnershipError; use drop_helpers::pause::PauseError; use neutron_sdk::NeutronError; @@ -24,6 +27,15 @@ pub enum ContractError { #[error("{0}")] OverflowError(#[from] OverflowError), + #[error("{0}")] + DivideByZeroError(#[from] DivideByZeroError), + + #[error("{0}")] + ConversionOverflowError(#[from] ConversionOverflowError), + + #[error("{0}")] + Decimal256RangeExceeded(#[from] Decimal256RangeExceeded), + #[error("Unauthorized")] Unauthorized {}, @@ -41,6 +53,8 @@ pub enum ContractError { #[error("Invalid denom")] InvalidDenom {}, + #[error("No delegations")] + NoDelegations {}, #[error("Idle min interval is not reached")] IdleMinIntervalIsNotReached {}, @@ -116,6 +130,9 @@ pub enum ContractError { #[error("Unbonded amount must be less or equal to expected amount")] UnbondedAmountTooHigh {}, + #[error("Validator info not found: {validator}")] + ValidatorInfoNotFound { validator: String }, + #[error("Fee must be in range [0.0, 1.0]")] InvalidFee {}, diff --git a/packages/base/src/msg/puppeteer.rs b/packages/base/src/msg/puppeteer.rs index 7d3e2a29..9deadc67 100644 --- a/packages/base/src/msg/puppeteer.rs +++ b/packages/base/src/msg/puppeteer.rs @@ -9,14 +9,11 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin as CosmosCoin; use drop_puppeteer_base::{ msg::{ExecuteMsg as BaseExecuteMsg, IBCTransferReason, TransferReadyBatchesMsg}, r#trait::PuppeteerReconstruct, - state::RedeemShareItem, + state::{Delegations, RedeemShareItem}, }; use neutron_sdk::{ bindings::types::StorageValue, - interchain_queries::v045::{ - helpers::deconstruct_account_denom_balance_key, - types::{Balances, Delegations}, - }, + interchain_queries::v045::{helpers::deconstruct_account_denom_balance_key, types::Balances}, }; use neutron_sdk::{NeutronError, NeutronResult}; use std::str::FromStr; diff --git a/packages/base/src/msg/tests.rs b/packages/base/src/msg/tests.rs index 9f706b03..429f51df 100644 --- a/packages/base/src/msg/tests.rs +++ b/packages/base/src/msg/tests.rs @@ -1,6 +1,6 @@ -use cosmwasm_std::{to_json_binary, Addr, Binary, Coin, Delegation, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Binary, Coin, Decimal256, Uint128}; use drop_puppeteer_base::r#trait::PuppeteerReconstruct; -use drop_puppeteer_base::state::BalancesAndDelegations; +use drop_puppeteer_base::state::{BalancesAndDelegations, DropDelegation}; use neutron_sdk::interchain_queries::v047::helpers::create_account_denom_balance_key; use neutron_sdk::NeutronResult; use neutron_sdk::{bindings::types::StorageValue, interchain_queries::helpers::decode_and_convert}; @@ -93,7 +93,7 @@ fn test_reconstruct_balance_and_delegations_no_delegations() { }]; assert_eq!(balances_and_delegations.balances.coins, expected_coins); - let expected_delegations: Vec = vec![]; + let expected_delegations: Vec = vec![]; assert_eq!( balances_and_delegations.delegations.delegations, expected_delegations @@ -134,7 +134,7 @@ fn test_reconstruct_balance_and_delegations_with_delegations() { let delegation = cosmos_sdk_proto::cosmos::staking::v1beta1::Delegation { delegator_address: "delegator".to_string(), validator_address: "validator".to_string(), - shares: "1000".to_string(), + shares: "1000000000000000000000".to_string(), }; let mut buf = Vec::new(); delegation.encode(&mut buf).unwrap(); @@ -150,7 +150,7 @@ fn test_reconstruct_balance_and_delegations_with_delegations() { jailed: false, status: 1, tokens: "1000".to_string(), - delegator_shares: "1000".to_string(), + delegator_shares: "1000000000000000000000".to_string(), description: None, unbonding_height: 0, unbonding_time: None, @@ -175,13 +175,14 @@ fn test_reconstruct_balance_and_delegations_with_delegations() { }]; assert_eq!(balances_and_delegations.balances.coins, expected_coins); - let expected_delegations: Vec = vec![Delegation { + let expected_delegations: Vec = vec![DropDelegation { delegator: Addr::unchecked("delegator"), validator: "validator".to_string(), amount: Coin { denom: "uatom".to_string(), amount: Uint128::from(1000u128), }, + share_ratio: Decimal256::one(), }]; assert_eq!( balances_and_delegations.delegations.delegations, diff --git a/packages/puppeteer-base/src/state.rs b/packages/puppeteer-base/src/state.rs index bdf41232..e08a72f3 100644 --- a/packages/puppeteer-base/src/state.rs +++ b/packages/puppeteer-base/src/state.rs @@ -2,7 +2,7 @@ use cosmos_sdk_proto::cosmos::{ base::v1beta1::Coin as CosmosCoin, staking::v1beta1::{Delegation, Params, Validator as CosmosValidator}, }; -use cosmwasm_std::{from_json, Addr, Decimal256, StdError, Timestamp, Uint128}; +use cosmwasm_std::{from_json, Addr, Decimal256, StdError, Timestamp, Uint128, Uint256}; use cosmwasm_schema::cw_serde; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, UniqueIndex}; @@ -10,7 +10,7 @@ use drop_helpers::{ica::Ica, version::version_to_u32}; use neutron_sdk::{ interchain_queries::v045::{ helpers::deconstruct_account_denom_balance_key, - types::{Balances, Delegations, UnbondingEntry}, + types::{Balances, UnbondingEntry}, }, NeutronError, NeutronResult, }; @@ -123,6 +123,22 @@ pub struct BalancesAndDelegations { pub delegations: Delegations, } +#[cw_serde] +pub struct Delegations { + pub delegations: Vec, +} + +#[cw_serde] +pub struct DropDelegation { + pub delegator: Addr, + /// A validator address (e.g. cosmosvaloper1...) + pub validator: String, + /// How much we have locked in the delegation + pub amount: cosmwasm_std::Coin, + /// How many shares the delegator has in the validator + pub share_ratio: Decimal256, +} + #[cw_serde] #[derive(Default)] pub enum TxStateStatus { @@ -210,7 +226,7 @@ impl PuppeteerReconstruct for BalancesAndDelegations { }?; coins.push(cosmwasm_std::Coin::new(amount.u128(), denom)); } - let mut delegations: Vec = + let mut delegations: Vec = Vec::with_capacity((storage_values.len() - 2) / 2); // first StorageValue is denom if !storage_values[1].value.is_empty() { @@ -231,10 +247,11 @@ impl PuppeteerReconstruct for BalancesAndDelegations { } let delegation_sdk: Delegation = Delegation::decode(chunk[0].value.as_slice())?; - let mut delegation_std = cosmwasm_std::Delegation { + let mut delegation_std = DropDelegation { delegator: Addr::unchecked(delegation_sdk.delegator_address.as_str()), validator: delegation_sdk.validator_address, amount: Default::default(), + share_ratio: Decimal256::one(), }; if chunk[1].value.is_empty() { @@ -267,12 +284,12 @@ impl PuppeteerReconstruct for BalancesAndDelegations { delegation_shares .checked_mul(validator_tokens)? .div(delegator_shares) - .atomics(), + .atomics() + / Uint256::from(DECIMAL_FRACTIONAL), ) .map_err(|err| NeutronError::Std(StdError::ConversionOverflow { source: err }))? - .u128() - .div(DECIMAL_FRACTIONAL); - + .u128(); + delegation_std.share_ratio = validator_tokens / delegator_shares; delegation_std.amount = cosmwasm_std::Coin::new(delegated_tokens, &denom); delegations.push(delegation_std);