From 034ec4375ce50e34d9e9aff15d1f86d497b27b81 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:08:57 +0400 Subject: [PATCH] feat(staking): add ability to call hook contract on stake action (#430) * feat(staking): add ability to call hook contract on stake action New endpoint enter_with_hook { contract_address: Addr, msg: Binary } allows stake ASTRO to xASTRO and immediately pass all resulting xASTRO to the hook contract. * refactor Cargo.toml * remove hook gas limit; add schemas --- Cargo.lock | 23 ++-- Cargo.toml | 2 +- contracts/tokenomics/staking/Cargo.toml | 5 +- contracts/tokenomics/staking/src/contract.rs | 74 ++++++++----- contracts/tokenomics/staking/src/migrate.rs | 2 +- .../tokenomics/staking/tests/common/helper.rs | 23 +++- .../staking/tests/staking_integration.rs | 103 +++++++++++++++++- packages/astroport/Cargo.toml | 2 +- packages/astroport/src/staking.rs | 12 +- .../astroport-staking/astroport-staking.json | 37 ++++++- schemas/astroport-staking/raw/execute.json | 34 +++++- .../astroport-staking/raw/instantiate.json | 1 + 12 files changed, 262 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73d0de4a..5eaf63be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ dependencies = [ [[package]] name = "astroport" -version = "5.3.0" +version = "5.4.0" dependencies = [ "astroport-circular-buffer 0.2.0", "cosmos-sdk-proto 0.19.0", @@ -196,7 +196,7 @@ name = "astroport-factory" version = "1.8.1" dependencies = [ "anyhow", - "astroport 5.3.0", + "astroport 5.4.0", "astroport-pair 2.0.2", "astroport-test", "cosmwasm-schema", @@ -245,7 +245,7 @@ version = "1.2.0" dependencies = [ "anyhow", "astro-token-converter", - "astroport 5.3.0", + "astroport 5.4.0", "astroport-factory", "astroport-native-coin-registry", "astroport-pair 2.0.2", @@ -341,7 +341,7 @@ dependencies = [ name = "astroport-pair" version = "2.0.2" dependencies = [ - "astroport 5.3.0", + "astroport 5.4.0", "astroport-factory", "astroport-incentives", "astroport-test", @@ -364,7 +364,7 @@ name = "astroport-pair-concentrated" version = "4.0.2" dependencies = [ "anyhow", - "astroport 5.3.0", + "astroport 5.4.0", "astroport-circular-buffer 0.2.0", "astroport-factory", "astroport-incentives", @@ -413,7 +413,7 @@ name = "astroport-pair-stable" version = "4.0.1" dependencies = [ "anyhow", - "astroport 5.3.0", + "astroport 5.4.0", "astroport-circular-buffer 0.2.0", "astroport-factory", "astroport-incentives", @@ -439,7 +439,7 @@ name = "astroport-pair-transmuter" version = "1.1.2" dependencies = [ "anyhow", - "astroport 5.3.0", + "astroport 5.4.0", "astroport-factory", "astroport-native-coin-registry", "astroport-test", @@ -459,7 +459,7 @@ dependencies = [ name = "astroport-pair-xyk-sale-tax" version = "2.0.2" dependencies = [ - "astroport 5.3.0", + "astroport 5.4.0", "astroport-factory", "astroport-incentives", "astroport-pair 1.3.3", @@ -485,7 +485,7 @@ name = "astroport-pcl-common" version = "2.0.2" dependencies = [ "anyhow", - "astroport 5.3.0", + "astroport 5.4.0", "astroport-factory", "astroport-test", "cosmwasm-schema", @@ -517,10 +517,11 @@ dependencies = [ [[package]] name = "astroport-staking" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "astroport 4.0.3", + "astroport 5.4.0", "astroport-tokenfactory-tracker 1.0.0", "cosmwasm-schema", "cosmwasm-std", @@ -538,7 +539,7 @@ name = "astroport-test" version = "0.1.0" dependencies = [ "anyhow", - "astroport 5.3.0", + "astroport 5.4.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index c0ba86a5..7c10ec11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ thiserror = "1.0" itertools = "0.12" cosmwasm-schema = "1.5" cw-utils = "1" -astroport = { path = "./packages/astroport", version = "5.3.0" } +astroport = { path = "./packages/astroport", version = "5.4.0" } [profile.release] opt-level = "z" diff --git a/contracts/tokenomics/staking/Cargo.toml b/contracts/tokenomics/staking/Cargo.toml index 82bbd22d..6116ea9a 100644 --- a/contracts/tokenomics/staking/Cargo.toml +++ b/contracts/tokenomics/staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-staking" -version = "2.1.0" +version = "2.2.0" authors = ["Astroport"] edition = "2021" description = "Astroport Staking Contract" @@ -28,7 +28,8 @@ cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } cw-storage-plus.workspace = true thiserror.workspace = true cw2.workspace = true -astroport = "4" +astroport.workspace = true +astroport_v4 = { package = "astroport", version = "4" } cw-utils.workspace = true osmosis-std = "0.21.0" diff --git a/contracts/tokenomics/staking/src/contract.rs b/contracts/tokenomics/staking/src/contract.rs index 47f06c6d..19a3776a 100644 --- a/contracts/tokenomics/staking/src/contract.rs +++ b/contracts/tokenomics/staking/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, coin, ensure, to_json_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, + attr, coin, ensure, to_json_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use cw2::set_contract_version; @@ -115,7 +115,31 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Enter { receiver } => execute_enter(deps, env, info, receiver), + ExecuteMsg::Enter { receiver } => { + // xASTRO is minted to the receiver if provided or to the sender. + let recipient = receiver.unwrap_or_else(|| info.sender.to_string()); + execute_enter(deps, env, info).map(|(resp, minted_coins)| { + resp.add_message(BankMsg::Send { + to_address: recipient.clone(), + amount: vec![minted_coins], + }) + .add_attributes([("action", "enter"), ("recipient", recipient.as_str())]) + }) + } + ExecuteMsg::EnterWithHook { + contract_address, + msg, + } => execute_enter(deps, env, info).map(|(resp, minted_coins)| { + resp.add_message(WasmMsg::Execute { + contract_addr: contract_address.clone(), + msg, + funds: vec![minted_coins], + }) + .add_attributes([ + ("action", "enter_with_hook"), + ("next_contract", &contract_address), + ]) + }), ExecuteMsg::Leave {} => execute_leave(deps, env, info), } } @@ -163,7 +187,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result, -) -> Result { +) -> Result<(Response, Coin), ContractError> { let config = CONFIG.load(deps.storage)?; // Ensure that the correct denom is sent. Sending zero tokens is prohibited on chain level @@ -255,7 +280,7 @@ fn execute_enter( let minted_coins = coin(mint_amount.u128(), config.xastro_denom); - // Mint new xASTRO tokens to the sender + // Mint new xASTRO tokens to the staking contract messages.push( MsgMint { sender: env.contract.address.to_string(), @@ -265,18 +290,6 @@ fn execute_enter( .into(), ); - let recipient = receiver.unwrap_or_else(|| info.sender.to_string()); - - // TokenFactory minting only allows minting to the sender for now, thus we - // need to send the minted tokens to the recipient - messages.push( - BankMsg::Send { - to_address: recipient.clone(), - amount: vec![minted_coins], - } - .into(), - ); - // Set the data to be returned in set_data to easy integration with // other contracts let staking_response = to_json_binary(&StakingResponse { @@ -284,15 +297,16 @@ fn execute_enter( xastro_amount: mint_amount, })?; - Ok(Response::new() - .add_messages(messages) - .set_data(staking_response) - .add_attributes([ - attr("action", "enter"), - attr("recipient", recipient), - attr("astro_amount", amount), - attr("xastro_amount", mint_amount), - ])) + Ok(( + Response::new() + .add_messages(messages) + .set_data(staking_response) + .add_attributes([ + attr("astro_amount", amount), + attr("xastro_amount", mint_amount), + ]), + minted_coins, + )) } /// Leave unstakes TokenFactory xASTRO for ASTRO. xASTRO is burned and ASTRO @@ -397,7 +411,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let tracker_config = TRACKER_DATA.load(deps.storage)?; deps.querier.query_wasm_smart( tracker_config.tracker_addr, - &astroport::tokenfactory_tracker::QueryMsg::BalanceAt { address, timestamp }, + &astroport_v4::tokenfactory_tracker::QueryMsg::BalanceAt { address, timestamp }, )? }; @@ -411,7 +425,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let tracker_config = TRACKER_DATA.load(deps.storage)?; deps.querier.query_wasm_smart( tracker_config.tracker_addr, - &astroport::tokenfactory_tracker::QueryMsg::TotalSupplyAt { timestamp }, + &astroport_v4::tokenfactory_tracker::QueryMsg::TotalSupplyAt { timestamp }, )? }; diff --git a/contracts/tokenomics/staking/src/migrate.rs b/contracts/tokenomics/staking/src/migrate.rs index 1ea1b2a0..a9a94066 100644 --- a/contracts/tokenomics/staking/src/migrate.rs +++ b/contracts/tokenomics/staking/src/migrate.rs @@ -14,7 +14,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result match contract_version.version.as_ref() { - "2.0.0" => {} + "2.0.0" | "2.1.0" => {} _ => return Err(ContractError::MigrationError {}), }, _ => return Err(ContractError::MigrationError {}), diff --git a/contracts/tokenomics/staking/tests/common/helper.rs b/contracts/tokenomics/staking/tests/common/helper.rs index 6d26afe3..884dc1d5 100644 --- a/contracts/tokenomics/staking/tests/common/helper.rs +++ b/contracts/tokenomics/staking/tests/common/helper.rs @@ -1,10 +1,11 @@ #![allow(dead_code)] use anyhow::Result as AnyResult; +use cosmwasm_schema::serde::Serialize; use cosmwasm_std::testing::MockApi; use cosmwasm_std::{ - coins, Addr, Coin, DepsMut, Empty, Env, GovMsg, IbcMsg, IbcQuery, MemoryStorage, MessageInfo, - Response, StdResult, Uint128, + coins, to_json_binary, Addr, Coin, DepsMut, Empty, Env, GovMsg, IbcMsg, IbcQuery, + MemoryStorage, MessageInfo, Response, StdResult, Uint128, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, @@ -130,6 +131,24 @@ impl Helper { ) } + pub fn stake_with_hook( + &mut self, + sender: &Addr, + amount: u128, + contract_address: String, + msg: &T, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.staking.clone(), + &ExecuteMsg::EnterWithHook { + contract_address, + msg: to_json_binary(msg)?, + }, + &coins(amount, ASTRO_DENOM), + ) + } + pub fn unstake(&mut self, sender: &Addr, amount: u128) -> AnyResult { self.app.execute_contract( sender.clone(), diff --git a/contracts/tokenomics/staking/tests/staking_integration.rs b/contracts/tokenomics/staking/tests/staking_integration.rs index 49eea7f3..24f29aae 100644 --- a/contracts/tokenomics/staking/tests/staking_integration.rs +++ b/contracts/tokenomics/staking/tests/staking_integration.rs @@ -2,8 +2,12 @@ use std::collections::HashMap; -use cosmwasm_std::{coin, coins, from_json, Addr, BlockInfo, Timestamp, Uint128}; -use cw_multi_test::{Executor, TOKEN_FACTORY_MODULE}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + coin, coins, from_json, Addr, BankMsg, Binary, BlockInfo, Deps, DepsMut, Empty, Env, + MessageInfo, Response, StdResult, Timestamp, Uint128, +}; +use cw_multi_test::{Contract, ContractWrapper, Executor, TOKEN_FACTORY_MODULE}; use cw_utils::PaymentError; use itertools::Itertools; @@ -513,3 +517,98 @@ fn test_different_query_results() { .unwrap(); assert_eq!(balance_none, balance_some); } + +#[test] +fn test_hooks() { + #[cw_serde] + enum AbsorberMsg { + Absorb {}, + SendTo { recipient: String }, + } + fn xastro_absorber_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + |_: DepsMut, _: Env, info: MessageInfo, msg: AbsorberMsg| -> StdResult { + match msg { + AbsorberMsg::Absorb {} => Ok(Response::new()), + AbsorberMsg::SendTo { recipient } => { + Ok(Response::new().add_message(BankMsg::Send { + to_address: recipient, + amount: info.funds, + })) + } + } + }, + |_: DepsMut, _: Env, _: MessageInfo, _: Empty| -> StdResult { + Ok(Response::new()) + }, + |_: Deps, _: Env, _: Empty| -> StdResult { unimplemented!() }, + )) + } + + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + helper.give_astro(10000, &owner); + helper.stake(&owner, 10000).unwrap(); + + let absorber_code_id = helper.app.store_code(xastro_absorber_contract()); + let absorber = helper + .app + .instantiate_contract( + absorber_code_id, + owner.clone(), + &Empty {}, + &[], + "absorber", + None, + ) + .unwrap(); + + let alice = Addr::unchecked("alice"); + helper.give_astro(30000, &alice); + + helper + .stake_with_hook(&alice, 10000, absorber.to_string(), &AbsorberMsg::Absorb {}) + .unwrap(); + let absorber_balance = helper + .app + .wrap() + .query_balance(&absorber, &helper.xastro_denom) + .unwrap() + .amount; + assert_eq!(absorber_balance.u128(), 10000); + + let bob = Addr::unchecked("bob"); + helper + .stake_with_hook( + &alice, + 10000, + absorber.to_string(), + &AbsorberMsg::SendTo { + recipient: bob.to_string(), + }, + ) + .unwrap(); + + // Absorber balance hasn't changed + let absorber_balance = helper + .app + .wrap() + .query_balance(&absorber, &helper.xastro_denom) + .unwrap() + .amount; + assert_eq!(absorber_balance.u128(), 10000); + + // Bob has received 10000 xASTRO + let bob_balance = helper + .app + .wrap() + .query_balance(&bob, &helper.xastro_denom) + .unwrap() + .amount; + assert_eq!(bob_balance.u128(), 10000); + + // Try failing hook msg + helper + .stake_with_hook(&alice, 10000, absorber.to_string(), &()) + .unwrap_err(); +} diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index da9a7af0..5563accd 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "5.3.0" +version = "5.4.0" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils" diff --git a/packages/astroport/src/staking.rs b/packages/astroport/src/staking.rs index ecc4185a..cde70ea2 100644 --- a/packages/astroport/src/staking.rs +++ b/packages/astroport/src/staking.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Uint128; +use cosmwasm_std::{Binary, Uint128}; /// This structure describes the parameters used for creating a contract. #[cw_serde] @@ -8,7 +8,7 @@ pub struct InstantiateMsg { pub deposit_token_denom: String, /// Tracking contract admin pub tracking_admin: String, - // The Code ID of contract used to track the TokenFactory token balances + /// The Code ID of contract used to track the TokenFactory token balances pub tracking_code_id: u64, /// Token factory module address. Contract creator must ensure that the address is exact token factory module address. pub token_factory_addr: String, @@ -20,6 +20,12 @@ pub enum ExecuteMsg { /// Deposits ASTRO in exchange for xASTRO /// The receiver is optional. If not set, the sender will receive the xASTRO. Enter { receiver: Option }, + /// Deposits ASTRO in exchange for xASTRO + /// and passes **all resulting xASTRO** to defined contract along with an executable message. + EnterWithHook { + contract_address: String, + msg: Binary, + }, /// Burns xASTRO in exchange for ASTRO Leave {}, } @@ -74,7 +80,7 @@ pub struct TrackerData { pub tracker_addr: String, } -// The structure returned as part of set_data when staking or unstaking +/// The structure returned as part of set_data when staking or unstaking #[cw_serde] pub struct StakingResponse { /// The ASTRO denom diff --git a/schemas/astroport-staking/astroport-staking.json b/schemas/astroport-staking/astroport-staking.json index 691010f1..684ae294 100644 --- a/schemas/astroport-staking/astroport-staking.json +++ b/schemas/astroport-staking/astroport-staking.json @@ -1,6 +1,6 @@ { "contract_name": "astroport-staking", - "contract_version": "2.1.0", + "contract_version": "2.2.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -27,6 +27,7 @@ "type": "string" }, "tracking_code_id": { + "description": "The Code ID of contract used to track the TokenFactory token balances", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -61,6 +62,32 @@ }, "additionalProperties": false }, + { + "description": "Deposits ASTRO in exchange for xASTRO and passes **all resulting xASTRO** to defined contract along with an executable message.", + "type": "object", + "required": [ + "enter_with_hook" + ], + "properties": { + "enter_with_hook": { + "type": "object", + "required": [ + "contract_address", + "msg" + ], + "properties": { + "contract_address": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Burns xASTRO in exchange for ASTRO", "type": "object", @@ -75,7 +102,13 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } }, "query": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/astroport-staking/raw/execute.json b/schemas/astroport-staking/raw/execute.json index 94308306..19ab48c2 100644 --- a/schemas/astroport-staking/raw/execute.json +++ b/schemas/astroport-staking/raw/execute.json @@ -25,6 +25,32 @@ }, "additionalProperties": false }, + { + "description": "Deposits ASTRO in exchange for xASTRO and passes **all resulting xASTRO** to defined contract along with an executable message.", + "type": "object", + "required": [ + "enter_with_hook" + ], + "properties": { + "enter_with_hook": { + "type": "object", + "required": [ + "contract_address", + "msg" + ], + "properties": { + "contract_address": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Burns xASTRO in exchange for ASTRO", "type": "object", @@ -39,5 +65,11 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } } diff --git a/schemas/astroport-staking/raw/instantiate.json b/schemas/astroport-staking/raw/instantiate.json index 1e91dc8d..d97ea0a9 100644 --- a/schemas/astroport-staking/raw/instantiate.json +++ b/schemas/astroport-staking/raw/instantiate.json @@ -23,6 +23,7 @@ "type": "string" }, "tracking_code_id": { + "description": "The Code ID of contract used to track the TokenFactory token balances", "type": "integer", "format": "uint64", "minimum": 0.0