diff --git a/contracts/core/src/contract.rs b/contracts/core/src/contract.rs index 9d1a3a01..8a0c4e4f 100644 --- a/contracts/core/src/contract.rs +++ b/contracts/core/src/contract.rs @@ -5,9 +5,15 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; use lido_staking_base::helpers::answer::response; -use lido_staking_base::msg::core::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use lido_staking_base::msg::token::ExecuteMsg as TokenExecuteMsg; -use lido_staking_base::state::core::CONFIG; +use lido_staking_base::msg::{ + core::{ExecuteMsg, InstantiateMsg, QueryMsg}, + token::ExecuteMsg as TokenExecuteMsg, + voucher::ExecuteMsg as VoucherExecuteMsg, +}; +use lido_staking_base::state::core::{ + UnbondBatchStatus, UnbondItem, CONFIG, UNBOND_BATCHES, UNBOND_BATCH_ID, +}; +use lido_staking_base::state::voucher::{Metadata, Trait}; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; use std::str::FromStr; use std::vec; @@ -22,7 +28,6 @@ pub fn instantiate( msg: InstantiateMsg, ) -> ContractResult> { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - CONFIG.save(deps.storage, &msg.clone().into())?; let attrs: Vec = vec![ attr("token_contract", msg.token_contract), @@ -30,6 +35,16 @@ pub fn instantiate( attr("strategy_contract", msg.strategy_contract), attr("owner", msg.owner), ]; + UNBOND_BATCH_ID.save(deps.storage, &0u128)?; + UNBOND_BATCHES.save( + deps.storage, + 0u128, + &lido_staking_base::state::core::UnbondBatch { + total_amount: Uint128::zero(), + unbond_items: vec![], + status: UnbondBatchStatus::New, + }, + )?; Ok(response("instantiate", CONTRACT_NAME, attrs)) } @@ -54,20 +69,21 @@ pub fn execute( ) -> ContractResult> { match msg { ExecuteMsg::Bond { receiver } => execute_bond(deps, env, info, receiver), - ExecuteMsg::Unbond { amount } => execute_unbond(deps, env, info, amount), + ExecuteMsg::Unbond {} => execute_unbond(deps, env, info), ExecuteMsg::UpdateConfig { token_contract, puppeteer_contract, strategy_contract, owner, + ld_denom, } => execute_update_config( deps, - env, info, token_contract, puppeteer_contract, strategy_contract, owner, + ld_denom, ), } } @@ -131,12 +147,12 @@ fn check_denom(_denom: String) -> ContractResult<()> { fn execute_update_config( deps: DepsMut, - _env: Env, info: MessageInfo, token_contract: Option, puppeteer_contract: Option, strategy_contract: Option, owner: Option, + ld_denom: Option, ) -> ContractResult> { let mut config = CONFIG.load(deps.storage)?; ensure_eq!(config.owner, info.sender, ContractError::Unauthorized {}); @@ -158,15 +174,88 @@ fn execute_update_config( config.owner = owner.clone(); attrs.push(attr("owner", owner)); } + if let Some(ld_denom) = ld_denom { + config.ld_denom = Some(ld_denom.clone()); + attrs.push(attr("ld_denom", ld_denom)); + } CONFIG.save(deps.storage, &config)?; Ok(response("execute-update_config", CONTRACT_NAME, attrs)) } fn execute_unbond( - _deps: DepsMut, - _env: Env, - _info: MessageInfo, - _amount: Uint128, + deps: DepsMut, + env: Env, + info: MessageInfo, ) -> ContractResult> { - unimplemented!("todo"); + let mut attrs = vec![attr("action", "unbond")]; + let unbond_batch_id = UNBOND_BATCH_ID.load(deps.storage)?; + if info.funds.len() != 1 { + return Err(ContractError::InvalidFunds { + reason: "Must be one token".to_string(), + }); + } + let config = CONFIG.load(deps.storage)?; + let ld_denom = config.ld_denom.ok_or(ContractError::LDDenomIsNotSet {})?; + let amount = info.funds[0].amount; + let denom = info.funds[0].denom.to_string(); + if denom != ld_denom { + return Err(ContractError::InvalidFunds { + reason: "Must be LD token".to_string(), + }); + } + let mut unbond_batch = UNBOND_BATCHES.load(deps.storage, unbond_batch_id)?; + unbond_batch.unbond_items.push(UnbondItem { + sender: info.sender.to_string(), + amount, + }); + let exchange_rate = query_exchange_rate(deps.as_ref(), env)?; + attrs.push(attr("exchange_rate", exchange_rate.to_string())); + let expected_amount = amount * exchange_rate; + attrs.push(attr("expected_amount", expected_amount.to_string())); + UNBOND_BATCHES.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, + expected_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: amount.to_string(), + }, + Trait { + display_type: None, + trait_type: "expected_amount".to_string(), + value: expected_amount.to_string(), + }, + Trait { + display_type: None, + trait_type: "exchange_rate".to_string(), + value: exchange_rate.to_string(), + }, + ]), + }); + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.voucher_contract, + 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.unbond_items.len().to_string(), + token_uri: None, + extension, + })?, + funds: vec![], + }); + + Ok(response("execute-unbond", CONTRACT_NAME, attrs).add_message(msg)) } diff --git a/contracts/core/src/error.rs b/contracts/core/src/error.rs index 24c4bd60..d6dc14e9 100644 --- a/contracts/core/src/error.rs +++ b/contracts/core/src/error.rs @@ -18,6 +18,9 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + + #[error("LD denom is not set")] + LDDenomIsNotSet {}, } pub type ContractResult = Result; diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs index a15ce21a..46366e9a 100644 --- a/contracts/factory/src/contract.rs +++ b/contracts/factory/src/contract.rs @@ -1,17 +1,22 @@ use crate::{ error::ContractResult, - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + msg::{CallbackMsg, ExecuteMsg, InstantiateMsg, QueryMsg}, state::{Config, State, CONFIG, STATE}, }; use cosmwasm_std::{ attr, entry_point, instantiate2_address, to_json_binary, Binary, CodeInfoResponse, CosmosMsg, - Deps, DepsMut, Env, HexBinary, MessageInfo, Response, StdResult, WasmMsg, + Deps, DepsMut, Env, HexBinary, MessageInfo, QueryRequest, Response, StdResult, WasmMsg, + WasmQuery, }; use cw2::set_contract_version; use lido_staking_base::{ - helpers::answer::response, msg::core::InstantiateMsg as CoreInstantiateMsg, - msg::token::InstantiateMsg as TokenInstantiateMsg, + helpers::answer::response, + msg::core::{ExecuteMsg as CoreExecuteMsg, InstantiateMsg as CoreInstantiateMsg}, + msg::token::{ + ConfigResponse as TokenConfigResponse, InstantiateMsg as TokenInstantiateMsg, + QueryMsg as TokenQueryMsg, + }, msg::voucher::InstantiateMsg as VoucherInstantiateMsg, }; use neutron_sdk::{ @@ -70,6 +75,9 @@ pub fn execute( ) -> ContractResult> { match msg { ExecuteMsg::Init {} => execute_init(deps, env, info), + ExecuteMsg::Callback(msg) => match msg { + CallbackMsg::PostInit {} => execute_post_init(deps, env, info), + }, } } @@ -127,6 +135,7 @@ fn execute_init( token_contract: token_contract.to_string(), puppeteer_contract: "".to_string(), strategy_contract: "".to_string(), + voucher_contract: voucher_contract.to_string(), owner: env.contract.address.to_string(), })?, funds: vec![], @@ -144,11 +153,42 @@ fn execute_init( funds: vec![], salt: Binary::from(salt), }), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::Callback(CallbackMsg::PostInit {}))?, + funds: vec![], + }), ]; Ok(response("execute-init", CONTRACT_NAME, attrs).add_messages(msgs)) } +fn execute_post_init( + deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> ContractResult> { + let attrs = vec![attr("action", "post_init")]; + let state = STATE.load(deps.storage)?; + let token_config: TokenConfigResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: state.token_contract, + msg: to_json_binary(&TokenQueryMsg::Config {})?, + }))?; + let core_update_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: state.core_contract, + msg: to_json_binary(&CoreExecuteMsg::UpdateConfig { + token_contract: None, + puppeteer_contract: None, + strategy_contract: None, + owner: None, + ld_denom: Some(token_config.denom), + })?, + funds: vec![], + }); + Ok(response("execute-post_init", CONTRACT_NAME, attrs).add_message(core_update_msg)) +} + fn get_code_checksum(deps: Deps, code_id: u64) -> NeutronResult { let CodeInfoResponse { checksum, .. } = deps.querier.query_wasm_code_info(code_id)?; Ok(checksum) diff --git a/contracts/factory/src/msg.rs b/contracts/factory/src/msg.rs index f271c0db..ac4dda3e 100644 --- a/contracts/factory/src/msg.rs +++ b/contracts/factory/src/msg.rs @@ -11,9 +11,15 @@ pub struct InstantiateMsg { pub subdenom: String, } +#[cw_serde] +pub enum CallbackMsg { + PostInit {}, +} + #[cw_serde] pub enum ExecuteMsg { Init {}, + Callback(CallbackMsg), } #[cw_serde] pub enum MigrateMsg {} diff --git a/integration_tests/src/generated/contractLib/lidoCore.ts b/integration_tests/src/generated/contractLib/lidoCore.ts index a485013e..15e93375 100644 --- a/integration_tests/src/generated/contractLib/lidoCore.ts +++ b/integration_tests/src/generated/contractLib/lidoCore.ts @@ -6,6 +6,7 @@ export interface InstantiateMsg { puppeteer_contract: string; strategy_contract: string; token_contract: string; + voucher_contract: string; } /** * A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0 @@ -13,39 +14,25 @@ export interface InstantiateMsg { * The greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18) */ export type Decimal256 = string; -/** - * A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. - * - * # Examples - * - * Use `from` to create instances of this and `u128` to get the value out: - * - * ``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123); - * - * let b = Uint128::from(42u64); assert_eq!(b.u128(), 42); - * - * let c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ``` - */ -export type Uint128 = string; export interface LidoCoreSchema { responses: Config | Decimal256; - execute: BondArgs | UnbondArgs | UpdateConfigArgs; + execute: BondArgs | UpdateConfigArgs; [k: string]: unknown; } export interface Config { + ld_denom?: string | null; owner: string; puppeteer_contract: string; strategy_contract: string; token_contract: string; + voucher_contract: string; } export interface BondArgs { receiver?: string | null; } -export interface UnbondArgs { - amount: Uint128; -} export interface UpdateConfigArgs { + ld_denom?: string | null; owner?: string | null; puppeteer_contract?: string | null; strategy_contract?: string | null; @@ -93,9 +80,9 @@ export class Client { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { bond: args }, fee || "auto", memo, funds); } - unbond = async(sender:string, args: UnbondArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + unbond = async(sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } - return this.client.execute(sender, this.contractAddress, { unbond: args }, fee || "auto", memo, funds); + return this.client.execute(sender, this.contractAddress, { unbond: {} }, fee || "auto", memo, funds); } updateConfig = async(sender:string, args: UpdateConfigArgs, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } diff --git a/integration_tests/src/generated/contractLib/lidoFactory.ts b/integration_tests/src/generated/contractLib/lidoFactory.ts index f0de0249..0e436bb8 100644 --- a/integration_tests/src/generated/contractLib/lidoFactory.ts +++ b/integration_tests/src/generated/contractLib/lidoFactory.ts @@ -56,4 +56,8 @@ export class Client { if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } return this.client.execute(sender, this.contractAddress, { init: {} }, fee || "auto", memo, funds); } + callback = async(sender: string, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]): Promise => { + if (!isSigningCosmWasmClient(this.client)) { throw this.mustBeSigningClient(); } + return this.client.execute(sender, this.contractAddress, { callback: {} }, fee || "auto", memo, funds); + } } diff --git a/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts b/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts index 10a08899..e07f4e43 100644 --- a/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts +++ b/integration_tests/src/generated/contractLib/lidoWithdrawalVoucher.ts @@ -330,6 +330,7 @@ export interface Metadata { attributes?: Trait[] | null; batch_id: string; description?: string | null; + expected_amount: Uint128; name: string; } export interface Trait { diff --git a/integration_tests/src/testcases/core.test.ts b/integration_tests/src/testcases/core.test.ts index 1336f0f0..35ef391c 100644 --- a/integration_tests/src/testcases/core.test.ts +++ b/integration_tests/src/testcases/core.test.ts @@ -55,6 +55,7 @@ describe('Core', () => { exchangeRate?: number; tokenContractAddress?: string; neutronIBCDenom?: string; + ldDenom?: string; } = {}; beforeAll(async () => { @@ -321,11 +322,28 @@ describe('Core', () => { await neutronClient.CosmosBankV1Beta1.query.queryAllBalances( neutronSecondUserAddress, ); - expect( - balances.data.balances.find((one) => one.denom.startsWith('factory')), - ).toEqual({ + const ldBalance = balances.data.balances.find((one) => + one.denom.startsWith('factory'), + ); + expect(ldBalance).toEqual({ denom: `factory/${context.tokenContractAddress}/lido`, amount: String(500_000 * context.exchangeRate), }); + context.ldDenom = ldBalance?.denom; + }); + it('unbond', async () => { + const { coreContractClient, neutronUserAddress, ldDenom } = context; + const res = await coreContractClient.unbond( + neutronUserAddress, + 1.6, + undefined, + [ + { + amount: (500_000 * context.exchangeRate).toString(), + denom: ldDenom, + }, + ], + ); + expect(res.transactionHash).toHaveLength(64); }); }); diff --git a/packages/base/src/msg/core.rs b/packages/base/src/msg/core.rs index 77f7cced..27c4f5b2 100644 --- a/packages/base/src/msg/core.rs +++ b/packages/base/src/msg/core.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Decimal256, Uint128}; +use cosmwasm_std::Decimal256; use crate::state::core::Config; @@ -8,6 +8,7 @@ pub struct InstantiateMsg { pub token_contract: String, pub puppeteer_contract: String, pub strategy_contract: String, + pub voucher_contract: String, pub owner: String, } @@ -25,15 +26,14 @@ pub enum ExecuteMsg { Bond { receiver: Option, }, - Unbond { - amount: Uint128, - }, + Unbond {}, //permissioned UpdateConfig { token_contract: Option, puppeteer_contract: Option, strategy_contract: Option, owner: Option, + ld_denom: Option, }, } #[cw_serde] @@ -45,7 +45,9 @@ impl From for Config { token_contract: val.token_contract, puppeteer_contract: val.puppeteer_contract, strategy_contract: val.strategy_contract, + voucher_contract: val.voucher_contract, owner: val.owner, + ld_denom: None, } } } diff --git a/packages/base/src/state/core.rs b/packages/base/src/state/core.rs index 4b978bc5..6f4556a9 100644 --- a/packages/base/src/state/core.rs +++ b/packages/base/src/state/core.rs @@ -1,12 +1,37 @@ use cosmwasm_schema::cw_serde; -use cw_storage_plus::Item; +use cosmwasm_std::Uint128; +use cw_storage_plus::{Item, Map}; #[cw_serde] pub struct Config { pub token_contract: String, pub puppeteer_contract: String, pub strategy_contract: String, + pub voucher_contract: String, pub owner: String, + pub ld_denom: Option, } - pub const CONFIG: Item = Item::new("config"); + +#[cw_serde] +pub struct UnbondItem { + pub sender: String, + pub amount: Uint128, +} + +#[cw_serde] +pub enum UnbondBatchStatus { + New, + Unbonding, + Unbonded, +} + +#[cw_serde] +pub struct UnbondBatch { + pub total_amount: Uint128, + pub unbond_items: Vec, + pub status: UnbondBatchStatus, +} + +pub const UNBOND_BATCHES: Map = Map::new("batches"); +pub const UNBOND_BATCH_ID: Item = Item::new("batches_ids"); diff --git a/packages/base/src/state/voucher.rs b/packages/base/src/state/voucher.rs index c634862e..6039f2c1 100644 --- a/packages/base/src/state/voucher.rs +++ b/packages/base/src/state/voucher.rs @@ -16,4 +16,5 @@ pub struct Metadata { pub attributes: Option>, pub batch_id: String, pub amount: Uint128, + pub expected_amount: Uint128, }