diff --git a/contracts/rewards-collector/base/src/contract.rs b/contracts/rewards-collector/base/src/contract.rs index 3b1c4d509..7615ab34a 100644 --- a/contracts/rewards-collector/base/src/contract.rs +++ b/contracts/rewards-collector/base/src/contract.rs @@ -8,6 +8,7 @@ use mars_red_bank_types::{ address_provider::{self, AddressResponseItem, MarsAddressType}, incentives, red_bank, rewards_collector::{ + credit_manager::{self, Action}, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, UpdateConfig, }, }; @@ -85,6 +86,10 @@ where denom, amount, } => self.withdraw_from_red_bank(deps, denom, amount), + ExecuteMsg::WithdrawFromCreditManager { + account_id, + actions, + } => self.withdraw_from_credit_manager(deps, account_id, actions), ExecuteMsg::DistributeRewards { denom, amount, @@ -194,6 +199,42 @@ where .add_attribute("amount", stringify_option_amount(amount))) } + pub fn withdraw_from_credit_manager( + &self, + deps: DepsMut, + account_id: String, + actions: Vec, + ) -> ContractResult> { + let cfg = self.config.load(deps.storage)?; + + let valid_actions = actions.iter().all(|action| { + matches!(action, Action::Withdraw(..) | Action::WithdrawLiquidity { .. }) + }); + if !valid_actions { + return Err(ContractError::InvalidActionsForCreditManager {}); + } + + let cm_addr = address_provider::helpers::query_contract_addr( + deps.as_ref(), + &cfg.address_provider, + MarsAddressType::CreditManager, + )?; + + let withdraw_from_cm_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cm_addr.to_string(), + msg: to_binary(&credit_manager::ExecuteMsg::UpdateCreditAccount { + account_id: account_id.clone(), + actions, + })?, + funds: vec![], + }); + + Ok(Response::new() + .add_message(withdraw_from_cm_msg) + .add_attribute("action", "withdraw_from_credit_manager") + .add_attribute("account_id", account_id)) + } + pub fn claim_incentive_rewards( &self, deps: DepsMut, diff --git a/contracts/rewards-collector/base/src/error.rs b/contracts/rewards-collector/base/src/error.rs index 83ab8d732..6744b846a 100644 --- a/contracts/rewards-collector/base/src/error.rs +++ b/contracts/rewards-collector/base/src/error.rs @@ -39,6 +39,9 @@ pub enum ContractError { InvalidRoute { reason: String, }, + + #[error("Invalid actions. Only Withdraw and WithdrawLiquidity is possible to pass for CreditManager")] + InvalidActionsForCreditManager {}, } pub type ContractResult = Result; diff --git a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/test_withdraw.rs index a2e5645aa..48d4b3662 100644 --- a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs +++ b/contracts/rewards-collector/osmosis/tests/test_withdraw.rs @@ -1,5 +1,9 @@ -use cosmwasm_std::{testing::mock_env, to_binary, CosmosMsg, SubMsg, Uint128, WasmMsg}; -use mars_red_bank_types::rewards_collector::ExecuteMsg; +use cosmwasm_std::{coin, testing::mock_env, to_binary, CosmosMsg, SubMsg, Uint128, WasmMsg}; +use mars_red_bank_types::rewards_collector::{ + credit_manager::{self, Action, ActionAmount, ActionCoin}, + ExecuteMsg, +}; +use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; use mars_testing::mock_info; @@ -37,3 +41,76 @@ fn withdrawing_from_red_bank() { })) ) } + +#[test] +fn withdrawing_from_cm_if_action_not_allowed() { + let mut deps = helpers::setup_test(); + + // anyone can execute a withdrawal + let error_res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromCreditManager { + account_id: "random_id".to_string(), + actions: vec![ + Action::Withdraw(coin(100u128, "uatom")), + Action::Unknown {}, + Action::WithdrawLiquidity { + lp_token: ActionCoin { + denom: "gamm/pool/1".to_string(), + amount: ActionAmount::AccountBalance, + }, + minimum_receive: vec![], + }, + ], + }, + ) + .unwrap_err(); + assert_eq!(error_res, ContractError::InvalidActionsForCreditManager {}); +} + +#[test] +fn withdrawing_from_cm_successfully() { + let mut deps = helpers::setup_test(); + + let account_id = "random_id".to_string(); + let actions = vec![ + Action::Withdraw(coin(100u128, "uusdc")), + Action::WithdrawLiquidity { + lp_token: ActionCoin { + denom: "gamm/pool/1".to_string(), + amount: ActionAmount::AccountBalance, + }, + minimum_receive: vec![], + }, + Action::Withdraw(coin(120u128, "uatom")), + Action::Withdraw(coin(140u128, "uosmo")), + ]; + + // anyone can execute a withdrawal + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromCreditManager { + account_id: account_id.clone(), + actions: actions.clone(), + }, + ) + .unwrap(); + + assert_eq!(res.messages.len(), 1); + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "credit_manager".to_string(), + msg: to_binary(&credit_manager::ExecuteMsg::UpdateCreditAccount { + account_id, + actions + }) + .unwrap(), + funds: vec![] + })) + ) +} diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs index 748e51c0a..149490eb5 100644 --- a/packages/types/src/rewards_collector.rs +++ b/packages/types/src/rewards_collector.rs @@ -6,6 +6,8 @@ use mars_utils::{ helpers::{decimal_param_le_one, integer_param_gt_zero, validate_native_denom}, }; +use self::credit_manager::Action; + const MAX_SLIPPAGE_TOLERANCE_PERCENTAGE: u64 = 50; #[cw_serde] @@ -130,6 +132,12 @@ pub enum ExecuteMsg { amount: Option, }, + /// Withdraw coins from the credit manager + WithdrawFromCreditManager { + account_id: String, + actions: Vec, + }, + /// Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, /// according to the split set in config. /// Callable by any address. @@ -191,3 +199,39 @@ pub enum QueryMsg { #[returns(ConfigResponse)] Config {}, } + +// TODO: rover is private repo for now so can't use it as a dependency. Use rover types once repo is public. +pub mod credit_manager { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Coin, Uint128}; + + #[cw_serde] + pub enum ExecuteMsg { + UpdateCreditAccount { + account_id: String, + actions: Vec, + }, + } + + #[cw_serde] + pub enum Action { + Withdraw(Coin), + WithdrawLiquidity { + lp_token: ActionCoin, + minimum_receive: Vec, + }, + Unknown {}, // Used to simulate allowance only for: Withdraw and WithdrawLiquidity + } + + #[cw_serde] + pub struct ActionCoin { + pub denom: String, + pub amount: ActionAmount, + } + + #[cw_serde] + pub enum ActionAmount { + Exact(Uint128), + AccountBalance, + } +} diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index e0b95cadc..30e68fddc 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -195,6 +195,35 @@ }, "additionalProperties": false }, + { + "description": "Withdraw coins from the credit manager", + "type": "object", + "required": [ + "withdraw_from_credit_manager" + ], + "properties": { + "withdraw_from_credit_manager": { + "type": "object", + "required": [ + "account_id", + "actions" + ], + "properties": { + "account_id": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, according to the split set in config. Callable by any address.", "type": "object", @@ -300,6 +329,101 @@ } ], "definitions": { + "Action": { + "oneOf": [ + { + "type": "object", + "required": [ + "withdraw" + ], + "properties": { + "withdraw": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "withdraw_liquidity" + ], + "properties": { + "withdraw_liquidity": { + "type": "object", + "required": [ + "lp_token", + "minimum_receive" + ], + "properties": { + "lp_token": { + "$ref": "#/definitions/ActionCoin" + }, + "minimum_receive": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unknown" + ], + "properties": { + "unknown": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ActionAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "account_balance" + ] + }, + { + "type": "object", + "required": [ + "exact" + ], + "properties": { + "exact": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "ActionCoin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/ActionAmount" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts index 44808d0c6..b52768c4a 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts @@ -15,7 +15,10 @@ import { Coin, ExecuteMsg, OwnerUpdate, + Action, + ActionAmount, UpdateConfig, + ActionCoin, QueryMsg, ConfigResponse, } from './MarsRewardsCollectorBase.types' @@ -73,6 +76,18 @@ export interface MarsRewardsCollectorBaseInterface memo?: string, _funds?: Coin[], ) => Promise + withdrawFromCreditManager: ( + { + accountId, + actions, + }: { + accountId: string + actions: Action[] + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise distributeRewards: ( { amount, @@ -128,6 +143,7 @@ export class MarsRewardsCollectorBaseClient this.updateOwner = this.updateOwner.bind(this) this.updateConfig = this.updateConfig.bind(this) this.withdrawFromRedBank = this.withdrawFromRedBank.bind(this) + this.withdrawFromCreditManager = this.withdrawFromCreditManager.bind(this) this.distributeRewards = this.distributeRewards.bind(this) this.swapAsset = this.swapAsset.bind(this) this.claimIncentiveRewards = this.claimIncentiveRewards.bind(this) @@ -199,6 +215,32 @@ export class MarsRewardsCollectorBaseClient _funds, ) } + withdrawFromCreditManager = async ( + { + accountId, + actions, + }: { + accountId: string + actions: Action[] + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + withdraw_from_credit_manager: { + account_id: accountId, + actions, + }, + }, + fee, + memo, + _funds, + ) + } distributeRewards = async ( { amount, diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts index 5db72cd7f..90b128d6b 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts @@ -16,7 +16,10 @@ import { Coin, ExecuteMsg, OwnerUpdate, + Action, + ActionAmount, UpdateConfig, + ActionCoin, QueryMsg, ConfigResponse, } from './MarsRewardsCollectorBase.types' @@ -130,6 +133,38 @@ export function useMarsRewardsCollectorBaseDistributeRewardsMutation( options, ) } +export interface MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation { + client: MarsRewardsCollectorBaseClient + msg: { + accountId: string + actions: Action[] + } + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsRewardsCollectorBaseWithdrawFromCreditManagerMutation( + options?: Omit< + UseMutationOptions< + ExecuteResult, + Error, + MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation + >, + 'mutationFn' + >, +) { + return useMutation< + ExecuteResult, + Error, + MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation + >( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.withdrawFromCreditManager(msg, fee, memo, funds), + options, + ) +} export interface MarsRewardsCollectorBaseWithdrawFromRedBankMutation { client: MarsRewardsCollectorBaseClient msg: { diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts index eaef259bb..fc16e839c 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts @@ -43,6 +43,12 @@ export type ExecuteMsg = denom: string } } + | { + withdraw_from_credit_manager: { + account_id: string + actions: Action[] + } + } | { distribute_rewards: { amount?: Uint128 | null @@ -77,6 +83,24 @@ export type OwnerUpdate = } } | 'clear_emergency_owner' +export type Action = + | { + withdraw: Coin + } + | { + withdraw_liquidity: { + lp_token: ActionCoin + minimum_receive: Coin[] + } + } + | { + unknown: {} + } +export type ActionAmount = + | 'account_balance' + | { + exact: Uint128 + } export interface UpdateConfig { address_provider?: string | null channel_id?: string | null @@ -87,6 +111,10 @@ export interface UpdateConfig { slippage_tolerance?: Decimal | null timeout_seconds?: number | null } +export interface ActionCoin { + amount: ActionAmount + denom: string +} export type QueryMsg = { config: {} }