From 2da10eb1e39dd0d18b5dd87d588985f6d21ad314 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 12 Aug 2025 10:35:54 +0300 Subject: [PATCH 01/16] feat: proxy ado general structure --- Cargo.lock | 22 +++ .../socket/andromeda-proxy/.cargo/config | 4 + contracts/socket/andromeda-proxy/Cargo.toml | 39 ++++ .../socket/andromeda-proxy/examples/schema.rs | 15 ++ .../socket/andromeda-proxy/src/contract.rs | 170 ++++++++++++++++++ .../socket/andromeda-proxy/src/interface.rs | 6 + contracts/socket/andromeda-proxy/src/lib.rs | 11 ++ contracts/socket/andromeda-proxy/src/mock.rs | 129 +++++++++++++ contracts/socket/andromeda-proxy/src/state.rs | 7 + packages/andromeda-socket/src/lib.rs | 1 + packages/andromeda-socket/src/proxy.rs | 86 +++++++++ 11 files changed, 490 insertions(+) create mode 100644 contracts/socket/andromeda-proxy/.cargo/config create mode 100644 contracts/socket/andromeda-proxy/Cargo.toml create mode 100644 contracts/socket/andromeda-proxy/examples/schema.rs create mode 100644 contracts/socket/andromeda-proxy/src/contract.rs create mode 100644 contracts/socket/andromeda-proxy/src/interface.rs create mode 100644 contracts/socket/andromeda-proxy/src/lib.rs create mode 100644 contracts/socket/andromeda-proxy/src/mock.rs create mode 100644 contracts/socket/andromeda-proxy/src/state.rs create mode 100644 packages/andromeda-socket/src/proxy.rs diff --git a/Cargo.lock b/Cargo.lock index be4c57217..e3156fbd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,6 +802,28 @@ dependencies = [ "cw20 2.0.0", ] +[[package]] +name = "andromeda-proxy" +version = "0.1.0" +dependencies = [ + "andromeda-socket", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema 2.2.2", + "cosmwasm-std 2.2.2", + "cw-multi-test", + "cw-orch", + "cw-orch-daemon 0.24.6", + "cw-storage-plus 2.0.0", + "cw-utils 2.0.0", + "cw2 2.0.0", + "cw20 2.0.0", + "osmosis-std", + "schemars 0.8.22", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "andromeda-rate-limiting-withdrawals" version = "2.1.1-b.4" diff --git a/contracts/socket/andromeda-proxy/.cargo/config b/contracts/socket/andromeda-proxy/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/socket/andromeda-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/socket/andromeda-proxy/Cargo.toml b/contracts/socket/andromeda-proxy/Cargo.toml new file mode 100644 index 000000000..602637ae1 --- /dev/null +++ b/contracts/socket/andromeda-proxy/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "andromeda-proxy" +version = "0.1.0" +edition = "2021" +rust-version = "1.86.0" + + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +schemars = "0.8.16" +serde = { workspace = true } +thiserror = { workspace = true } +cw-utils = { workspace = true } +andromeda-std = { workspace = true } +cw20 = { workspace = true } +andromeda-socket = { workspace = true } +osmosis-std = "0.27.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +cw-orch = { workspace = true } +andromeda-testing = { workspace = true, optional = true } +cw-orch-daemon = "0.24.2" diff --git a/contracts/socket/andromeda-proxy/examples/schema.rs b/contracts/socket/andromeda-proxy/examples/schema.rs new file mode 100644 index 000000000..ddc49b8dc --- /dev/null +++ b/contracts/socket/andromeda-proxy/examples/schema.rs @@ -0,0 +1,15 @@ +use andromeda_socket::proxy::{Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::{export_schema_with_title, schema_for, write_api}; +use std::env::current_dir; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + }; + // Add this if there is a cw20 receive integration + export_schema_with_title(&schema_for!(Cw20HookMsg), &out_dir, "cw20receive"); +} diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs new file mode 100644 index 000000000..d90907810 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -0,0 +1,170 @@ +use crate::state::{ADMINS, LOCKED}; +use andromeda_socket::proxy::{ + AllLockedResponse, ExecuteMsg, InstantiateMsg, LockedInfo, LockedResponse, QueryMsg, +}; +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + amp::AndrAddr, + andr_execute_fn, + common::{context::ExecuteContext, encode_binary}, + error::ContractError, +}; +use cosmwasm_std::{ + ensure, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, SubMsg, +}; +use cosmwasm_std::{entry_point, CosmosMsg, WasmMsg}; +use cw2::set_contract_version; + +const CONTRACT_NAME: &str = "crates.io:andromeda-proxy"; +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, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let inst_resp = ADOContract::default().instantiate( + deps.storage, + env, + deps.api, + &deps.querier, + info.clone(), + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address.clone(), + owner: msg.owner, + }, + )?; + + ADMINS.save(deps.storage, &msg.admins)?; + + Ok(inst_resp + .add_attribute("method", "instantiate") + .add_attribute("owner", info.sender)) +} + +#[andr_execute_fn] +pub fn execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + match msg { + ExecuteMsg::Execute { + contract_addr, + message, + } => send_execute(ctx, contract_addr, message), + // ExecuteMsg::Receive(msg) => execute_receive(ctx, msg), + _ => ADOContract::default().execute(ctx, msg), + } +} + +// fn execute_receive( +// ctx: ExecuteContext, +// receive_msg: Cw20ReceiveMsg, +// ) -> Result { +// match from_json(&receive_msg.msg)? { +// Cw20HookMsg::Lock { recipient } => { +// let cw20_addr = ctx.info.sender.clone(); +// let user_addr = ctx.deps.api.addr_validate(&receive_msg.sender)?; +// let amount = receive_msg.amount; +// let recipient = recipient +// .map(|r| r.get_raw_address(&ctx.deps.as_ref())) +// .transpose()? +// .unwrap_or(user_addr); +// } +// } +// } + +// Used for cross-chain creation of denom +fn send_execute( + ctx: ExecuteContext, + contract_addr: AndrAddr, + message: Binary, +) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + // Check authority + let original_sender = ctx.amp_ctx.and_then(|pkt| Some(pkt.ctx.get_origin())); + let admins = ADMINS.load(deps.storage)?; + + let sender_to_check = match original_sender { + Some(sender) => sender, + // The user could eventually authorize his wallet address on the chain, so the message doesn't have to come from as an AMP Packet + None => info.sender.to_string(), + }; + + ensure!( + admins.contains(&sender_to_check), + ContractError::Unauthorized {} + ); + + // Forward the message + let sub_msg = SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.get_raw_address(&deps.as_ref())?.into_string(), + msg: message, + funds: info.funds, + })); + + Ok(Response::default() + .add_submessage(sub_msg) + .add_attribute("action", "send_execute")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Locked { cw20_addr } => encode_binary(&query_locked(deps, cw20_addr)?), + + QueryMsg::AllLocked {} => encode_binary(&query_all_locked(deps)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, env, CONTRACT_NAME, CONTRACT_VERSION) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, _msg: Reply) -> Result { + Ok(Response::default()) + // match msg.id { + // OSMOSIS_MSG_BURN_ID => { + // // Send IBC packet to unlock the cw20 + // if msg.result.is_err() { + // Err(ContractError::Std(StdError::generic_err(format!( + // "Osmosis swap failed with error: {:?}", + // msg.result.unwrap_err() + // )))) + // } else { + // Ok(Response::default().add_attributes(vec![attr("action", "token_burned")])) + // } + // } + // _ => Err(ContractError::Std(StdError::generic_err( + // "Invalid Reply ID".to_string(), + // ))), + // } +} + +fn query_locked(deps: Deps, cw20_addr: Addr) -> Result { + let amount = LOCKED + .may_load(deps.storage, cw20_addr)? + .unwrap_or_default(); + Ok(LockedResponse { amount }) +} + +fn query_all_locked(deps: Deps) -> Result { + let locked: Vec = LOCKED + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .filter_map(|item| { + let (cw20_addr, amount) = item.ok()?; + if !amount.is_zero() { + Some(LockedInfo { cw20_addr, amount }) + } else { + None + } + }) + .collect(); + Ok(AllLockedResponse { locked }) +} diff --git a/contracts/socket/andromeda-proxy/src/interface.rs b/contracts/socket/andromeda-proxy/src/interface.rs new file mode 100644 index 000000000..86d367e55 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/interface.rs @@ -0,0 +1,6 @@ +use andromeda_socket::proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ado_base::MigrateMsg, contract_interface, deploy::ADOMetadata}; + +pub const CONTRACT_ID: &str = "osmosis-proxy"; + +contract_interface!(ProxyContract, CONTRACT_ID, "andromeda_proxy.wasm"); diff --git a/contracts/socket/andromeda-proxy/src/lib.rs b/contracts/socket/andromeda-proxy/src/lib.rs new file mode 100644 index 000000000..694ba68c0 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +pub mod state; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(not(target_arch = "wasm32"))] +mod interface; + +#[cfg(not(target_arch = "wasm32"))] +pub use crate::interface::ProxyContract; diff --git a/contracts/socket/andromeda-proxy/src/mock.rs b/contracts/socket/andromeda-proxy/src/mock.rs new file mode 100644 index 000000000..d4f0db75a --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/mock.rs @@ -0,0 +1,129 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] + +use crate::contract::{execute, instantiate, query}; +use andromeda_socket::proxy::{ + AllLockedResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, +}; +use andromeda_std::ado_base::permissioning::{Permission, PermissioningMessage}; +use andromeda_std::ado_base::rates::{Rate, RatesMessage}; +use andromeda_std::amp::messages::AMPPkt; +use andromeda_std::amp::AndrAddr; + +use andromeda_testing::mock::MockApp; +use andromeda_testing::{ + mock_ado, + mock_contract::{ExecuteResult, MockADO, MockContract}, +}; +use cosmwasm_std::{Addr, Empty}; +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; + +pub struct MockProxy(Addr); +mock_ado!(MockProxy, ExecuteMsg, QueryMsg); + +impl MockProxy { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + + kernel_address: impl Into, + owner: Option, + ) -> MockProxy { + let msg = mock_osmosis_token_factory_instantiate_msg(kernel_address, owner); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Osmosis Token Factory", + Some(sender.to_string()), + ) + .unwrap(); + MockProxy(Addr::unchecked(addr)) + } + + pub fn execute_create_denom( + &self, + app: &mut MockApp, + sender: Addr, + subdenom: String, + ) -> AppResponse { + let msg = mock_create_denom(subdenom); + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + .unwrap() + } + + pub fn execute_add_rate( + &self, + app: &mut MockApp, + sender: Addr, + action: String, + rate: Rate, + ) -> ExecuteResult { + self.execute(app, &mock_set_rate_msg(action, rate), sender, &[]) + } + + pub fn execute_set_permission( + &self, + app: &mut MockApp, + sender: Addr, + actors: Vec, + action: String, + permission: Permission, + ) -> ExecuteResult { + let msg = mock_set_permission(actors, action, permission); + self.execute(app, &msg, sender, &[]) + } + + pub fn query_all_locked(&self, app: &MockApp) -> AllLockedResponse { + self.query::(app, query_all_locked()) + } +} + +pub fn mock_andromeda_osmosis_token_factory() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_osmosis_token_factory_instantiate_msg( + kernel_address: impl Into, + owner: Option, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address: kernel_address.into(), + owner, + } +} + +pub fn mock_create_denom(subdenom: String) -> ExecuteMsg { + ExecuteMsg::CreateDenom { subdenom } +} + +pub fn mock_set_rate_msg(action: String, rate: Rate) -> ExecuteMsg { + ExecuteMsg::Rates(RatesMessage::SetRate { action, rate }) +} + +pub fn mock_set_permission( + actors: Vec, + action: String, + permission: Permission, +) -> ExecuteMsg { + ExecuteMsg::Permissioning(PermissioningMessage::SetPermission { + actors, + action, + permission, + }) +} + +pub fn mock_cw20_hook_msg(recipient: Option) -> Cw20HookMsg { + Cw20HookMsg::Lock { recipient } +} + +pub fn mock_receive_packet(packet: AMPPkt) -> ExecuteMsg { + ExecuteMsg::AMPReceive(packet) +} + +pub fn query_all_locked() -> QueryMsg { + QueryMsg::AllLocked {} +} diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs new file mode 100644 index 000000000..8bfa6b461 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -0,0 +1,7 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + +/// Maps cw20_address -> locked_amount +pub const LOCKED: Map = Map::new("locked"); + +pub const ADMINS: Item> = Item::new("admins"); diff --git a/packages/andromeda-socket/src/lib.rs b/packages/andromeda-socket/src/lib.rs index 2649781ec..1b1ceff11 100644 --- a/packages/andromeda-socket/src/lib.rs +++ b/packages/andromeda-socket/src/lib.rs @@ -1,3 +1,4 @@ pub mod astroport; pub mod osmosis; pub mod osmosis_token_factory; +pub mod proxy; diff --git a/packages/andromeda-socket/src/proxy.rs b/packages/andromeda-socket/src/proxy.rs new file mode 100644 index 000000000..22f15237c --- /dev/null +++ b/packages/andromeda-socket/src/proxy.rs @@ -0,0 +1,86 @@ +use andromeda_std::amp::AndrAddr; +use andromeda_std::error::ContractError; +use andromeda_std::{andr_exec, andr_instantiate, andr_query}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, Env, QuerierWrapper, QueryRequest, Uint128, WasmQuery, +}; +use cw20::{Cw20QueryMsg, Cw20ReceiveMsg, TokenInfoResponse}; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub admins: Vec, +} + +#[andr_exec] +#[cw_serde] +#[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::ExecuteFns))] +pub enum ExecuteMsg { + #[cfg_attr(not(target_arch = "wasm32"), cw_orch(payable))] + Execute { + contract_addr: AndrAddr, + message: Binary, + // Funds will be native + }, + + ModifyAdmins { + admins: Vec, + }, + + Receive(Cw20ReceiveMsg), +} + +#[cw_serde] +pub enum Cw20HookMsg { + /// Lock the received CW20 tokens and mint factory tokens + Lock { recipient: Option }, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::QueryFns))] +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(LockedResponse)] + Locked { cw20_addr: Addr }, + + #[returns(AllLockedResponse)] + AllLocked {}, +} + +#[cw_serde] +pub struct LockedResponse { + pub amount: Uint128, +} + +#[cw_serde] +pub struct FactoryDenomResponse { + pub denom: Option, +} + +#[cw_serde] +pub struct LockedInfo { + pub cw20_addr: Addr, + pub amount: Uint128, +} + +#[cw_serde] +pub struct AllLockedResponse { + pub locked: Vec, +} + +/// The structure of the newly created denom is: “factory/{osmosis_socket_addr}/{subdenom}}” +pub fn get_factory_denom(env: &Env, subdenom: &str) -> String { + format!("factory/{}/{}", env.contract.address, subdenom) +} + +/// Checks if an address is a cw20 contract by querying the TokenInfo which is guaranteed to error if the address is a wallet +pub fn is_cw20_contract(querier: &QuerierWrapper, address: &str) -> Result { + Ok(querier + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: address.to_string(), + msg: to_json_binary(&Cw20QueryMsg::TokenInfo {})?, + })) + .is_ok()) +} From b5a7ac4ca009c4352bc7717e4a0ce5df4b694ac2 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 12 Aug 2025 10:40:26 +0300 Subject: [PATCH 02/16] docs: comments --- contracts/socket/andromeda-proxy/src/contract.rs | 5 +++-- contracts/socket/andromeda-proxy/src/state.rs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index d90907810..892756bb2 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -84,7 +84,8 @@ fn send_execute( message: Binary, ) -> Result { let ExecuteContext { deps, info, .. } = ctx; - // Check authority + + // Fetch original sender of the amp packet (if available) let original_sender = ctx.amp_ctx.and_then(|pkt| Some(pkt.ctx.get_origin())); let admins = ADMINS.load(deps.storage)?; @@ -94,6 +95,7 @@ fn send_execute( None => info.sender.to_string(), }; + // Check authority ensure!( admins.contains(&sender_to_check), ContractError::Unauthorized {} @@ -115,7 +117,6 @@ fn send_execute( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::Locked { cw20_addr } => encode_binary(&query_locked(deps, cw20_addr)?), - QueryMsg::AllLocked {} => encode_binary(&query_all_locked(deps)?), _ => ADOContract::default().query(deps, env, msg), } diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs index 8bfa6b461..2959b3901 100644 --- a/contracts/socket/andromeda-proxy/src/state.rs +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -4,4 +4,5 @@ use cw_storage_plus::{Item, Map}; /// Maps cw20_address -> locked_amount pub const LOCKED: Map = Map::new("locked"); +/// List of addresses that are allowed to operate this ADO pub const ADMINS: Item> = Item::new("admins"); From cdf1465433438942281d8dbeb7af722b8d8065ce Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 12 Aug 2025 10:52:03 +0300 Subject: [PATCH 03/16] ref: authorize helper function --- .../socket/andromeda-proxy/src/contract.rs | 60 ++++++------------- contracts/socket/andromeda-proxy/src/state.rs | 27 ++++++++- packages/andromeda-socket/src/proxy.rs | 4 +- 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index 892756bb2..cdfdfd9c6 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -1,4 +1,4 @@ -use crate::state::{ADMINS, LOCKED}; +use crate::state::{authorize, ADMINS, LOCKED}; use andromeda_socket::proxy::{ AllLockedResponse, ExecuteMsg, InstantiateMsg, LockedInfo, LockedResponse, QueryMsg, }; @@ -10,10 +10,8 @@ use andromeda_std::{ common::{context::ExecuteContext, encode_binary}, error::ContractError, }; -use cosmwasm_std::{ - ensure, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, SubMsg, -}; use cosmwasm_std::{entry_point, CosmosMsg, WasmMsg}; +use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, SubMsg}; use cw2::set_contract_version; const CONTRACT_NAME: &str = "crates.io:andromeda-proxy"; @@ -55,57 +53,26 @@ pub fn execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result send_execute(ctx, contract_addr, message), - // ExecuteMsg::Receive(msg) => execute_receive(ctx, msg), + ExecuteMsg::ModifyAdmins { admins } => execute_modify_admins(ctx, admins), _ => ADOContract::default().execute(ctx, msg), } } -// fn execute_receive( -// ctx: ExecuteContext, -// receive_msg: Cw20ReceiveMsg, -// ) -> Result { -// match from_json(&receive_msg.msg)? { -// Cw20HookMsg::Lock { recipient } => { -// let cw20_addr = ctx.info.sender.clone(); -// let user_addr = ctx.deps.api.addr_validate(&receive_msg.sender)?; -// let amount = receive_msg.amount; -// let recipient = recipient -// .map(|r| r.get_raw_address(&ctx.deps.as_ref())) -// .transpose()? -// .unwrap_or(user_addr); -// } -// } -// } - // Used for cross-chain creation of denom fn send_execute( ctx: ExecuteContext, contract_addr: AndrAddr, message: Binary, ) -> Result { - let ExecuteContext { deps, info, .. } = ctx; - - // Fetch original sender of the amp packet (if available) - let original_sender = ctx.amp_ctx.and_then(|pkt| Some(pkt.ctx.get_origin())); - let admins = ADMINS.load(deps.storage)?; - - let sender_to_check = match original_sender { - Some(sender) => sender, - // The user could eventually authorize his wallet address on the chain, so the message doesn't have to come from as an AMP Packet - None => info.sender.to_string(), - }; - - // Check authority - ensure!( - admins.contains(&sender_to_check), - ContractError::Unauthorized {} - ); + authorize(&ctx)?; // Forward the message let sub_msg = SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: contract_addr.get_raw_address(&deps.as_ref())?.into_string(), + contract_addr: contract_addr + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), msg: message, - funds: info.funds, + funds: ctx.info.funds, })); Ok(Response::default() @@ -113,6 +80,17 @@ fn send_execute( .add_attribute("action", "send_execute")) } +fn execute_modify_admins( + ctx: ExecuteContext, + admins: Vec, +) -> Result { + authorize(&ctx)?; + + ADMINS.save(ctx.deps.storage, &admins)?; + + Ok(Response::default().add_attribute("action", "modify_admins")) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs index 2959b3901..6ab1bd70a 100644 --- a/contracts/socket/andromeda-proxy/src/state.rs +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Addr, Uint128}; +use andromeda_std::{common::context::ExecuteContext, error::ContractError}; +use cosmwasm_std::{ensure, Addr, Uint128}; use cw_storage_plus::{Item, Map}; /// Maps cw20_address -> locked_amount @@ -6,3 +7,27 @@ pub const LOCKED: Map = Map::new("locked"); /// List of addresses that are allowed to operate this ADO pub const ADMINS: Item> = Item::new("admins"); + +/// If message is sent through AMP, it checks if the orignial sender is authorized. +/// If it's a direct message, it checks if the latest sender of the message is authorized. +pub(crate) fn authorize(ctx: &ExecuteContext) -> Result<(), ContractError> { + // Fetch original sender of the amp packet (if available) + let original_sender = ctx + .amp_ctx + .clone() + .and_then(|pkt| Some(pkt.ctx.get_origin())); + let admins = ADMINS.load(ctx.deps.storage)?; + + let sender_to_check = match original_sender { + Some(sender) => sender, + // The user could eventually authorize his wallet address on the chain, so the message doesn't have to come from as an AMP Packet + None => ctx.info.sender.to_string(), + }; + + // Check authority + ensure!( + admins.contains(&sender_to_check), + ContractError::Unauthorized {} + ); + Ok(()) +} diff --git a/packages/andromeda-socket/src/proxy.rs b/packages/andromeda-socket/src/proxy.rs index 22f15237c..f4369ce03 100644 --- a/packages/andromeda-socket/src/proxy.rs +++ b/packages/andromeda-socket/src/proxy.rs @@ -5,7 +5,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ to_json_binary, Addr, Binary, Env, QuerierWrapper, QueryRequest, Uint128, WasmQuery, }; -use cw20::{Cw20QueryMsg, Cw20ReceiveMsg, TokenInfoResponse}; +use cw20::{Cw20QueryMsg, TokenInfoResponse}; #[andr_instantiate] #[cw_serde] @@ -27,8 +27,6 @@ pub enum ExecuteMsg { ModifyAdmins { admins: Vec, }, - - Receive(Cw20ReceiveMsg), } #[cw_serde] From a2162d2cdd07aa9ce346c49f4a852a51848d5461 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 12 Aug 2025 11:07:14 +0300 Subject: [PATCH 04/16] feat: send_instantiate --- .../socket/andromeda-proxy/src/contract.rs | 36 ++++++++++++++++++- packages/andromeda-socket/src/proxy.rs | 14 ++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index cdfdfd9c6..88974f048 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -1,6 +1,6 @@ use crate::state::{authorize, ADMINS, LOCKED}; use andromeda_socket::proxy::{ - AllLockedResponse, ExecuteMsg, InstantiateMsg, LockedInfo, LockedResponse, QueryMsg, + AllLockedResponse, ExecuteMsg, InitParams, InstantiateMsg, LockedInfo, LockedResponse, QueryMsg, }; use andromeda_std::{ ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, @@ -49,6 +49,12 @@ pub fn instantiate( #[andr_execute_fn] pub fn execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result { match msg { + ExecuteMsg::Instantiate { + init_params, + message, + admin, + label, + } => send_instantiate(ctx, init_params, message, admin, label), ExecuteMsg::Execute { contract_addr, message, @@ -80,6 +86,34 @@ fn send_execute( .add_attribute("action", "send_execute")) } +fn send_instantiate( + ctx: ExecuteContext, + init_params: InitParams, + message: Binary, + admin: Option, + label: Option, +) -> Result { + authorize(&ctx)?; + + let code_id = match init_params { + InitParams::CodeId(code_id) => code_id, + InitParams::AdoVersion(adoversion) => todo!(), + }; + + // Forward the message + let sub_msg = SubMsg::new(CosmosMsg::Wasm(WasmMsg::Instantiate { + admin, + code_id, + msg: message, + funds: ctx.info.funds, + label: label.unwrap_or("default".to_string()), + })); + + Ok(Response::default() + .add_attribute("action", "send_instantiate") + .add_submessage(sub_msg)) +} + fn execute_modify_admins( ctx: ExecuteContext, admins: Vec, diff --git a/packages/andromeda-socket/src/proxy.rs b/packages/andromeda-socket/src/proxy.rs index f4369ce03..a6a987a21 100644 --- a/packages/andromeda-socket/src/proxy.rs +++ b/packages/andromeda-socket/src/proxy.rs @@ -1,5 +1,6 @@ use andromeda_std::amp::AndrAddr; use andromeda_std::error::ContractError; +use andromeda_std::os::adodb::ADOVersion; use andromeda_std::{andr_exec, andr_instantiate, andr_query}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ @@ -13,10 +14,23 @@ pub struct InstantiateMsg { pub admins: Vec, } +#[cw_serde] +pub enum InitParams { + CodeId(u64), + AdoVersion(ADOVersion), +} + #[andr_exec] #[cw_serde] #[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::ExecuteFns))] pub enum ExecuteMsg { + Instantiate { + init_params: InitParams, + message: Binary, + admin: Option, + label: Option, + }, + #[cfg_attr(not(target_arch = "wasm32"), cw_orch(payable))] Execute { contract_addr: AndrAddr, From 415b0f63dd31ae4775022df61ef6feb7685a06b9 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 12 Aug 2025 15:27:38 +0300 Subject: [PATCH 05/16] feat: handle ado version init param --- contracts/socket/andromeda-proxy/src/contract.rs | 9 ++++++++- contracts/socket/andromeda-proxy/src/state.rs | 5 +---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index 88974f048..956475f22 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -9,6 +9,7 @@ use andromeda_std::{ andr_execute_fn, common::{context::ExecuteContext, encode_binary}, error::ContractError, + os::aos_querier::AOSQuerier, }; use cosmwasm_std::{entry_point, CosmosMsg, WasmMsg}; use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, SubMsg}; @@ -97,7 +98,13 @@ fn send_instantiate( let code_id = match init_params { InitParams::CodeId(code_id) => code_id, - InitParams::AdoVersion(adoversion) => todo!(), + InitParams::AdoVersion(ado_version) => { + let adodb_addr = AOSQuerier::adodb_address_getter( + &ctx.deps.querier, + &ctx.contract.get_kernel_address(ctx.deps.storage)?, + )?; + AOSQuerier::code_id_getter(&ctx.deps.querier, &adodb_addr, ado_version.as_str())? + } }; // Forward the message diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs index 6ab1bd70a..0c6f82d82 100644 --- a/contracts/socket/andromeda-proxy/src/state.rs +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -12,10 +12,7 @@ pub const ADMINS: Item> = Item::new("admins"); /// If it's a direct message, it checks if the latest sender of the message is authorized. pub(crate) fn authorize(ctx: &ExecuteContext) -> Result<(), ContractError> { // Fetch original sender of the amp packet (if available) - let original_sender = ctx - .amp_ctx - .clone() - .and_then(|pkt| Some(pkt.ctx.get_origin())); + let original_sender = ctx.amp_ctx.clone().map(|pkt| pkt.ctx.get_origin()); let admins = ADMINS.load(ctx.deps.storage)?; let sender_to_check = match original_sender { From f3c980adec18dd03c8773974efd5b8760d7675f7 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 13 Aug 2025 09:59:12 +0300 Subject: [PATCH 06/16] test: general structure --- Cargo.lock | 2 ++ tests/ibc-tests/Cargo.toml | 10 ++++++++++ tests/ibc-tests/mod.rs | 5 ++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e3156fbd8..8f1aa6593 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4318,6 +4318,8 @@ dependencies = [ "andromeda-kernel", "andromeda-math", "andromeda-non-fungible-tokens", + "andromeda-proxy", + "andromeda-socket", "andromeda-splitter", "andromeda-std", "andromeda-testing", diff --git a/tests/ibc-tests/Cargo.toml b/tests/ibc-tests/Cargo.toml index 5d78cd067..8d89e2d42 100644 --- a/tests/ibc-tests/Cargo.toml +++ b/tests/ibc-tests/Cargo.toml @@ -18,6 +18,10 @@ path = "interchain.rs" name = "splitter-ibc" path = "splitter_ibc.rs" +[[test]] +name = "proxy" +path = "proxy.rs" + [dev-dependencies] andromeda-kernel = {path = "../../contracts/os/andromeda-kernel", features = [ @@ -26,11 +30,14 @@ andromeda-kernel = {path = "../../contracts/os/andromeda-kernel", features = [ andromeda-fixed-amount-splitter = { path = "../../contracts/finance/andromeda-fixed-amount-splitter", features = [ "testing", ] } + andromeda-app = { workspace = true } andromeda-std = { workspace = true } andromeda-testing = { workspace = true } andromeda-finance = { workspace = true } andromeda-math = { workspace = true } +andromeda-socket = { workspace = true } + andromeda-counter = { path = "../../contracts/math/andromeda-counter", features = [ "testing", ] } @@ -46,6 +53,9 @@ andromeda-auction = { path = "../../contracts/non-fungible-tokens/andromeda-auct andromeda-cw721 = { path = "../../contracts/non-fungible-tokens/andromeda-cw721", features = [ "testing", ] } +andromeda-proxy = { path = "../../contracts/socket/andromeda-proxy", features = [ + "testing", +] } andromeda-non-fungible-tokens = { workspace = true } cosmwasm-std = { workspace = true, features = ["staking"] } cw-multi-test = { workspace = true, features = ["cosmwasm_1_2", "staking"]} diff --git a/tests/ibc-tests/mod.rs b/tests/ibc-tests/mod.rs index 1fdff58a4..0b412c901 100644 --- a/tests/ibc-tests/mod.rs +++ b/tests/ibc-tests/mod.rs @@ -8,4 +8,7 @@ mod interchain; mod splitter_ibc; #[cfg(test)] -mod test_ibc; \ No newline at end of file +mod test_ibc; + +#[cfg(test)] +mod proxy; From 79f1526787673e127505a0f6c3454364c363edb6 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 13 Aug 2025 10:00:00 +0300 Subject: [PATCH 07/16] wip: test --- tests/ibc-tests/proxy.rs | 148 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 tests/ibc-tests/proxy.rs diff --git a/tests/ibc-tests/proxy.rs b/tests/ibc-tests/proxy.rs new file mode 100644 index 000000000..d8af7cb50 --- /dev/null +++ b/tests/ibc-tests/proxy.rs @@ -0,0 +1,148 @@ +#![cfg(not(target_arch = "wasm32"))] + +use andromeda_kernel::ack::make_ack_success; +use andromeda_proxy::ProxyContract; +use andromeda_std::{ + amp::{messages::AMPMsg, AndrAddr, Recipient}, + os, +}; +use andromeda_testing::{interchain::ensure_packet_success, InterchainTestEnv}; +use cosmwasm_std::{to_json_binary, Coin, Uint128}; +use cw_orch::{mock::cw_multi_test::ibc::types::keccak256, prelude::*}; +use cw_orch_interchain::prelude::*; + +#[test] +fn test_fixed_amount_splitter_ibc() { + let InterchainTestEnv { + juno, + osmosis, + interchain, + .. + } = InterchainTestEnv::new(); + + let owner_on_osmosis = osmosis.chain.addr_make("ownerosmo"); + let owner_on_juno = juno.chain.addr_make("ownerjuno"); + + // Deploy on Osmosis + let proxy_osmosis = ProxyContract::new(osmosis.chain.clone()); + proxy_osmosis.upload().unwrap(); + + let admins = vec![owner_on_juno.to_string()]; + // Owner on osmosis will init the proxy on osmo, and set his juno address as admin + proxy_osmosis + .instantiate( + &andromeda_socket::proxy::InstantiateMsg { + admins: admins, + kernel_address: osmosis.aos.kernel.address().unwrap().into_string(), + owner: None, + }, + None, + &[], + ) + .unwrap(); + + // Register contract + osmosis + .aos + .adodb + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: proxy_osmosis.code_id().unwrap(), + ado_type: "proxy".to_string(), + action_fees: None, + version: "0.1.0".to_string(), + publisher: None, + }, + &[], + ) + .unwrap(); + + // Create IBC message + let osmosis_recipient = AndrAddr::from_string(format!( + "ibc://{}/{}", + osmosis.chain_name, + proxy_osmosis.address().unwrap() + )); + + let message = AMPMsg::new( + osmosis_recipient, + to_json_binary( + &andromeda_finance::fixed_amount_splitter::ExecuteMsg::Send { config: None }, + ) + .unwrap(), + None, + ); + + // Execute IBC transfer from Juno + let kernel_juno_send_request = juno + .aos + .kernel + .execute( + &os::kernel::ExecuteMsg::Send { message }, + &[Coin { + amount: Uint128::new(100000000), + denom: juno.denom.clone(), + }], + ) + .unwrap(); + + // Wait for packet processing + let packet_lifetime = interchain + .await_packets(&juno.chain_id, kernel_juno_send_request) + .unwrap(); + ensure_packet_success(packet_lifetime); + + // Check balances + let balances = osmosis + .chain + .query_all_balances(&osmosis.aos.kernel.address().unwrap()) + .unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, expected_denom.clone()); + assert_eq!(balances[0].amount.u128(), 100000000); + + // Setup trigger + juno.aos + .kernel + .execute( + &os::kernel::ExecuteMsg::UpsertKeyAddress { + key: "trigger_key".to_string(), + value: juno.chain.sender.to_string(), + }, + &[], + ) + .unwrap(); + + let packet_ack = make_ack_success(); + let channel_id = juno + .aos + .get_aos_channel(&osmosis.chain_name) + .unwrap() + .ics20 + .unwrap(); + + // Execute trigger relay + let kernel_juno_splitter_request = juno + .aos + .kernel + .execute( + &os::kernel::ExecuteMsg::TriggerRelay { + packet_sequence: 1, + packet_ack, + channel_id, + }, + &[], + ) + .unwrap(); + + let packet_lifetime = interchain + .await_packets(&juno.chain_id, kernel_juno_splitter_request) + .unwrap(); + ensure_packet_success(packet_lifetime); + + // Verify final recipient balance + let balances = osmosis.chain.query_all_balances(&recipient).unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, expected_denom); + assert_eq!(balances[0].amount.u128(), 100); +} From 0f01594569d6d266cee7a69cd14c61a2d626f658 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 14 Aug 2025 10:23:59 +0300 Subject: [PATCH 08/16] test: init using proxy --- .../socket/andromeda-proxy/src/contract.rs | 58 ++++---- contracts/socket/andromeda-proxy/src/mock.rs | 23 +--- tests/ibc-tests/proxy.rs | 127 +++++++++--------- 3 files changed, 99 insertions(+), 109 deletions(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index 956475f22..c3c9d68ce 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -11,8 +11,10 @@ use andromeda_std::{ error::ContractError, os::aos_querier::AOSQuerier, }; -use cosmwasm_std::{entry_point, CosmosMsg, WasmMsg}; -use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, SubMsg}; +use cosmwasm_std::{ + attr, entry_point, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, SubMsg, WasmMsg, +}; use cw2::set_contract_version; const CONTRACT_NAME: &str = "crates.io:andromeda-proxy"; @@ -108,13 +110,16 @@ fn send_instantiate( }; // Forward the message - let sub_msg = SubMsg::new(CosmosMsg::Wasm(WasmMsg::Instantiate { - admin, - code_id, - msg: message, - funds: ctx.info.funds, - label: label.unwrap_or("default".to_string()), - })); + let sub_msg = SubMsg::reply_always( + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin, + code_id, + msg: message, + funds: ctx.info.funds, + label: label.unwrap_or("default".to_string()), + }), + 1, + ); Ok(Response::default() .add_attribute("action", "send_instantiate") @@ -147,24 +152,23 @@ pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result Result { - Ok(Response::default()) - // match msg.id { - // OSMOSIS_MSG_BURN_ID => { - // // Send IBC packet to unlock the cw20 - // if msg.result.is_err() { - // Err(ContractError::Std(StdError::generic_err(format!( - // "Osmosis swap failed with error: {:?}", - // msg.result.unwrap_err() - // )))) - // } else { - // Ok(Response::default().add_attributes(vec![attr("action", "token_burned")])) - // } - // } - // _ => Err(ContractError::Std(StdError::generic_err( - // "Invalid Reply ID".to_string(), - // ))), - // } +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + 1 => { + if msg.result.is_err() { + Err(ContractError::Std(StdError::generic_err(format!( + "Contract instantiation error: {:?}", + msg.result.unwrap_err() + )))) + } else { + Ok(Response::default() + .add_attributes(vec![attr("action", "contract_instantiated")])) + } + } + _ => Err(ContractError::Std(StdError::generic_err( + "Invalid Reply ID".to_string(), + ))), + } } fn query_locked(deps: Deps, cw20_addr: Addr) -> Result { diff --git a/contracts/socket/andromeda-proxy/src/mock.rs b/contracts/socket/andromeda-proxy/src/mock.rs index d4f0db75a..765cdcf4a 100644 --- a/contracts/socket/andromeda-proxy/src/mock.rs +++ b/contracts/socket/andromeda-proxy/src/mock.rs @@ -15,7 +15,7 @@ use andromeda_testing::{ mock_contract::{ExecuteResult, MockADO, MockContract}, }; use cosmwasm_std::{Addr, Empty}; -use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; +use cw_multi_test::{Contract, ContractWrapper, Executor}; pub struct MockProxy(Addr); mock_ado!(MockProxy, ExecuteMsg, QueryMsg); @@ -25,11 +25,11 @@ impl MockProxy { code_id: u64, sender: Addr, app: &mut MockApp, - + admins: Vec, kernel_address: impl Into, owner: Option, ) -> MockProxy { - let msg = mock_osmosis_token_factory_instantiate_msg(kernel_address, owner); + let msg = mock_osmosis_token_factory_instantiate_msg(admins, kernel_address, owner); let addr = app .instantiate_contract( code_id, @@ -43,17 +43,6 @@ impl MockProxy { MockProxy(Addr::unchecked(addr)) } - pub fn execute_create_denom( - &self, - app: &mut MockApp, - sender: Addr, - subdenom: String, - ) -> AppResponse { - let msg = mock_create_denom(subdenom); - app.execute_contract(sender, self.addr().clone(), &msg, &[]) - .unwrap() - } - pub fn execute_add_rate( &self, app: &mut MockApp, @@ -87,19 +76,17 @@ pub fn mock_andromeda_osmosis_token_factory() -> Box> { } pub fn mock_osmosis_token_factory_instantiate_msg( + admins: Vec, kernel_address: impl Into, owner: Option, ) -> InstantiateMsg { InstantiateMsg { + admins, kernel_address: kernel_address.into(), owner, } } -pub fn mock_create_denom(subdenom: String) -> ExecuteMsg { - ExecuteMsg::CreateDenom { subdenom } -} - pub fn mock_set_rate_msg(action: String, rate: Rate) -> ExecuteMsg { ExecuteMsg::Rates(RatesMessage::SetRate { action, rate }) } diff --git a/tests/ibc-tests/proxy.rs b/tests/ibc-tests/proxy.rs index d8af7cb50..e48ee7558 100644 --- a/tests/ibc-tests/proxy.rs +++ b/tests/ibc-tests/proxy.rs @@ -1,21 +1,22 @@ #![cfg(not(target_arch = "wasm32"))] -use andromeda_kernel::ack::make_ack_success; +use andromeda_fixed_amount_splitter::FixedAmountSplitterContract; use andromeda_proxy::ProxyContract; +use andromeda_socket::proxy::InitParams; use andromeda_std::{ amp::{messages::AMPMsg, AndrAddr, Recipient}, os, }; use andromeda_testing::{interchain::ensure_packet_success, InterchainTestEnv}; use cosmwasm_std::{to_json_binary, Coin, Uint128}; -use cw_orch::{mock::cw_multi_test::ibc::types::keccak256, prelude::*}; +use cw_orch::prelude::*; use cw_orch_interchain::prelude::*; #[test] -fn test_fixed_amount_splitter_ibc() { +fn test_proxy_ibc() { let InterchainTestEnv { - juno, - osmosis, + mut juno, + mut osmosis, interchain, .. } = InterchainTestEnv::new(); @@ -27,7 +28,11 @@ fn test_fixed_amount_splitter_ibc() { let proxy_osmosis = ProxyContract::new(osmosis.chain.clone()); proxy_osmosis.upload().unwrap(); - let admins = vec![owner_on_juno.to_string()]; + // This contract will eventually be instantiated by the proxy contract + let splitter_osmosis = FixedAmountSplitterContract::new(osmosis.chain.clone()); + splitter_osmosis.upload().unwrap(); + + let admins = vec![owner_on_juno.to_string(), owner_on_osmosis.to_string()]; // Owner on osmosis will init the proxy on osmo, and set his juno address as admin proxy_osmosis .instantiate( @@ -58,91 +63,85 @@ fn test_fixed_amount_splitter_ibc() { .unwrap(); // Create IBC message - let osmosis_recipient = AndrAddr::from_string(format!( + let proxy_osmosis_recipient = AndrAddr::from_string(format!( "ibc://{}/{}", osmosis.chain_name, proxy_osmosis.address().unwrap() )); + let splitter_init_msg = andromeda_finance::fixed_amount_splitter::InstantiateMsg { + recipients: vec![andromeda_finance::fixed_amount_splitter::AddressAmount { + recipient: Recipient { + address: AndrAddr::from_string(owner_on_osmosis.to_string()), + msg: None, + ibc_recovery_address: None, + }, + coins: vec![Coin { + denom: "osmo".to_string(), + amount: Uint128::new(100), + }], + }], + default_recipient: None, + lock_time: None, + kernel_address: osmosis.aos.kernel.address().unwrap().into_string(), + owner: None, + }; + let message = AMPMsg::new( - osmosis_recipient, - to_json_binary( - &andromeda_finance::fixed_amount_splitter::ExecuteMsg::Send { config: None }, - ) + proxy_osmosis_recipient, + to_json_binary(&andromeda_socket::proxy::ExecuteMsg::Instantiate { + init_params: InitParams::CodeId(splitter_osmosis.code_id().unwrap()), + message: to_json_binary(&splitter_init_msg).unwrap(), + admin: None, + label: None, + }) .unwrap(), None, ); - // Execute IBC transfer from Juno - let kernel_juno_send_request = juno + // Register username + osmosis .aos - .kernel + .vfs + .set_sender(&osmosis.aos.kernel.address().unwrap()); + + osmosis + .aos + .vfs .execute( - &os::kernel::ExecuteMsg::Send { message }, - &[Coin { - amount: Uint128::new(100000000), - denom: juno.denom.clone(), - }], + &os::vfs::ExecuteMsg::RegisterUser { + username: "steve".to_string(), + address: Some(owner_on_osmosis), + }, + &[], ) .unwrap(); - // Wait for packet processing - let packet_lifetime = interchain - .await_packets(&juno.chain_id, kernel_juno_send_request) - .unwrap(); - ensure_packet_success(packet_lifetime); - - // Check balances - let balances = osmosis - .chain - .query_all_balances(&osmosis.aos.kernel.address().unwrap()) - .unwrap(); - assert_eq!(balances.len(), 1); - assert_eq!(balances[0].denom, expected_denom.clone()); - assert_eq!(balances[0].amount.u128(), 100000000); - - // Setup trigger + juno.aos.vfs.set_sender(&juno.aos.kernel.address().unwrap()); juno.aos - .kernel + .vfs .execute( - &os::kernel::ExecuteMsg::UpsertKeyAddress { - key: "trigger_key".to_string(), - value: juno.chain.sender.to_string(), + &os::vfs::ExecuteMsg::RegisterUser { + username: "steve".to_string(), + address: Some(owner_on_juno.clone()), }, &[], ) .unwrap(); - let packet_ack = make_ack_success(); - let channel_id = juno - .aos - .get_aos_channel(&osmosis.chain_name) - .unwrap() - .ics20 - .unwrap(); - - // Execute trigger relay - let kernel_juno_splitter_request = juno + // Execute IBC msg from Juno + juno.aos.kernel.set_sender(&owner_on_juno); + let kernel_juno_send_request = juno .aos .kernel - .execute( - &os::kernel::ExecuteMsg::TriggerRelay { - packet_sequence: 1, - packet_ack, - channel_id, - }, - &[], - ) + .execute(&os::kernel::ExecuteMsg::Send { message }, &[]) .unwrap(); + splitter_osmosis.addr_str().unwrap_err(); + + // Wait for packet processing let packet_lifetime = interchain - .await_packets(&juno.chain_id, kernel_juno_splitter_request) + .await_packets(&juno.chain_id, kernel_juno_send_request) .unwrap(); ensure_packet_success(packet_lifetime); - - // Verify final recipient balance - let balances = osmosis.chain.query_all_balances(&recipient).unwrap(); - assert_eq!(balances.len(), 1); - assert_eq!(balances[0].denom, expected_denom); - assert_eq!(balances[0].amount.u128(), 100); } From 4cdb022795673e9ca7d99bb593be0f9fc9ff7106 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 14 Aug 2025 14:01:19 +0300 Subject: [PATCH 09/16] refactor: kernel ibc preserves hops, extract hop address for validation in proxy --- contracts/os/andromeda-kernel/src/execute.rs | 25 +++++++++++---- contracts/os/andromeda-kernel/src/ibc.rs | 26 +++++++-------- .../src/tests/test_handler.rs | 4 +-- contracts/socket/andromeda-proxy/src/state.rs | 19 +++++------ packages/std/src/amp/messages.rs | 4 +++ tests/ibc-tests/proxy.rs | 32 +------------------ 6 files changed, 46 insertions(+), 64 deletions(-) diff --git a/contracts/os/andromeda-kernel/src/execute.rs b/contracts/os/andromeda-kernel/src/execute.rs index f8b7df2b5..6619fab5c 100644 --- a/contracts/os/andromeda-kernel/src/execute.rs +++ b/contracts/os/andromeda-kernel/src/execute.rs @@ -116,14 +116,25 @@ pub fn handle_local( let sub_msg = if config.direct || ado_type.is_none() { amp_message.generate_sub_msg_direct(recipient_addr, ReplyId::AMPMsg.repr()) } else { - let origin = ctx.map_or(info.sender.to_string(), |ctx| ctx.get_origin()); + let origin = ctx + .clone() + .map_or(info.sender.to_string(), |ctx| ctx.get_origin()); let previous_sender = info.sender.to_string(); - - AMPPkt::new(origin, previous_sender, vec![amp_message.clone()]).to_sub_msg( - recipient_addr, - Some(funds.clone()), - ReplyId::AMPMsg.repr(), - )? + println!("origin: {origin}"); + println!("prev sender: {previous_sender}"); + if let Some(ctx) = ctx { + AMPPkt::new_with_ctx(ctx, vec![amp_message.clone()]).to_sub_msg( + recipient_addr, + Some(funds.clone()), + ReplyId::AMPMsg.repr(), + )? + } else { + AMPPkt::new(origin, previous_sender, vec![amp_message.clone()]).to_sub_msg( + recipient_addr, + Some(funds.clone()), + ReplyId::AMPMsg.repr(), + )? + } }; Ok(Response::default() diff --git a/contracts/os/andromeda-kernel/src/ibc.rs b/contracts/os/andromeda-kernel/src/ibc.rs index df73096ef..9a4df8b8e 100644 --- a/contracts/os/andromeda-kernel/src/ibc.rs +++ b/contracts/os/andromeda-kernel/src/ibc.rs @@ -18,10 +18,10 @@ use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure, from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, - Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, - IbcChannelConnectMsg, IbcChannelOpenMsg, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, - IbcPacketTimeoutMsg, IbcReceiveResponse, MessageInfo, SubMsg, WasmMsg, + ensure, from_json, to_json_binary, Binary, Deps, DepsMut, Empty, Env, Ibc3ChannelOpenResponse, + IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, + MessageInfo, SubMsg, WasmMsg, }; pub const PACKET_LIFETIME: u64 = 604_800u64; @@ -138,14 +138,13 @@ pub fn do_ibc_packet_receive( deps.branch(), MessageInfo { funds: vec![], - sender: Addr::unchecked( - "cosmwasm122xa328nvn93rsemr980psc9m9qwh8xj8rdje4qtp68m5tyt7yusajjrpz", - ), + sender: env.clone().contract.address, }, env.clone(), ); match packet_msg { IbcExecuteMsg::SendMessage { amp_packet } => { + let previous_hops = amp_packet.ctx.get_previous_hops(); // Try to pull the username's address from the packet match amp_packet.ctx.get_origin_username() { Some(username) => { @@ -163,12 +162,13 @@ pub fn do_ibc_packet_receive( } } None => { - // Added this to keep track of original sender, even if the address is invalid on the receiving chain - let new_amp_packet = AMPPkt::new( - amp_packet.ctx.get_origin(), - env.contract.address, - amp_packet.messages.clone(), - ); + let mut ctx = + AMPCtx::new(env.clone().contract.address, env.contract.address, None); + // Add previous hops to the context + for hop in previous_hops { + ctx.add_hop(hop); + } + let new_amp_packet = AMPPkt::new_with_ctx(ctx, amp_packet.messages.clone()); execute_env.amp_ctx = Some(new_amp_packet); } } diff --git a/contracts/os/andromeda-kernel/src/tests/test_handler.rs b/contracts/os/andromeda-kernel/src/tests/test_handler.rs index 5424781ed..0e5072993 100644 --- a/contracts/os/andromeda-kernel/src/tests/test_handler.rs +++ b/contracts/os/andromeda-kernel/src/tests/test_handler.rs @@ -75,7 +75,7 @@ fn test_handle_local() { ctx: Some(AMPCtx::new("origin", MOCK_APP_CONTRACT, None)), expected_submessage: AMPPkt::new( "origin", - SENDER, + "cosmwasm1t3hmmkn74pqs5htv9swgzpjf7np9z8zc98hgj8hx879cp5juk4eqmnwcm4", vec![AMPMsg::new( MOCK_APP_CONTRACT, to_json_binary(&true).unwrap(), @@ -101,7 +101,7 @@ fn test_handle_local() { ctx: Some(AMPCtx::new("origin", MOCK_APP_CONTRACT, None)), expected_submessage: AMPPkt::new( "origin", - SENDER, + "cosmwasm1t3hmmkn74pqs5htv9swgzpjf7np9z8zc98hgj8hx879cp5juk4eqmnwcm4", vec![AMPMsg::new( MOCK_APP_CONTRACT, to_json_binary(&true).unwrap(), diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs index 0c6f82d82..609b5ba55 100644 --- a/contracts/socket/andromeda-proxy/src/state.rs +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -12,19 +12,16 @@ pub const ADMINS: Item> = Item::new("admins"); /// If it's a direct message, it checks if the latest sender of the message is authorized. pub(crate) fn authorize(ctx: &ExecuteContext) -> Result<(), ContractError> { // Fetch original sender of the amp packet (if available) - let original_sender = ctx.amp_ctx.clone().map(|pkt| pkt.ctx.get_origin()); - let admins = ADMINS.load(ctx.deps.storage)?; + let sender = ctx + .amp_ctx + .clone() + .and_then(|pkt| pkt.ctx.get_previous_hops().first().cloned()) + .map(|hop| hop.address.to_string()) + .unwrap_or(ctx.info.sender.to_string()); - let sender_to_check = match original_sender { - Some(sender) => sender, - // The user could eventually authorize his wallet address on the chain, so the message doesn't have to come from as an AMP Packet - None => ctx.info.sender.to_string(), - }; + let admins = ADMINS.load(ctx.deps.storage)?; // Check authority - ensure!( - admins.contains(&sender_to_check), - ContractError::Unauthorized {} - ); + ensure!(admins.contains(&sender), ContractError::Unauthorized {}); Ok(()) } diff --git a/packages/std/src/amp/messages.rs b/packages/std/src/amp/messages.rs index f84065349..89080c96c 100644 --- a/packages/std/src/amp/messages.rs +++ b/packages/std/src/amp/messages.rs @@ -255,6 +255,10 @@ impl AMPCtx { self.previous_sender.clone() } + pub fn get_previous_hops(&self) -> Vec { + self.previous_hops.clone() + } + /// Gets the previous sender of a message pub fn get_id(&self) -> Option { self.id.clone() diff --git a/tests/ibc-tests/proxy.rs b/tests/ibc-tests/proxy.rs index e48ee7558..1d30d729b 100644 --- a/tests/ibc-tests/proxy.rs +++ b/tests/ibc-tests/proxy.rs @@ -16,7 +16,7 @@ use cw_orch_interchain::prelude::*; fn test_proxy_ibc() { let InterchainTestEnv { mut juno, - mut osmosis, + osmosis, interchain, .. } = InterchainTestEnv::new(); @@ -99,36 +99,6 @@ fn test_proxy_ibc() { None, ); - // Register username - osmosis - .aos - .vfs - .set_sender(&osmosis.aos.kernel.address().unwrap()); - - osmosis - .aos - .vfs - .execute( - &os::vfs::ExecuteMsg::RegisterUser { - username: "steve".to_string(), - address: Some(owner_on_osmosis), - }, - &[], - ) - .unwrap(); - - juno.aos.vfs.set_sender(&juno.aos.kernel.address().unwrap()); - juno.aos - .vfs - .execute( - &os::vfs::ExecuteMsg::RegisterUser { - username: "steve".to_string(), - address: Some(owner_on_juno.clone()), - }, - &[], - ) - .unwrap(); - // Execute IBC msg from Juno juno.aos.kernel.set_sender(&owner_on_juno); let kernel_juno_send_request = juno From a25ec5ae81a1834dda43f0212eb86c3a6504cf2a Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 14 Aug 2025 15:15:25 +0300 Subject: [PATCH 10/16] perf: add hops directly instead of looping through them --- contracts/os/andromeda-kernel/src/ibc.rs | 24 +++++++++++++----------- packages/std/src/amp/messages.rs | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/contracts/os/andromeda-kernel/src/ibc.rs b/contracts/os/andromeda-kernel/src/ibc.rs index 9a4df8b8e..d25076842 100644 --- a/contracts/os/andromeda-kernel/src/ibc.rs +++ b/contracts/os/andromeda-kernel/src/ibc.rs @@ -162,12 +162,13 @@ pub fn do_ibc_packet_receive( } } None => { - let mut ctx = - AMPCtx::new(env.clone().contract.address, env.contract.address, None); - // Add previous hops to the context - for hop in previous_hops { - ctx.add_hop(hop); - } + let ctx = AMPCtx::new_with_hops( + env.clone().contract.address, + env.contract.address, + None, + previous_hops, + ); + let new_amp_packet = AMPPkt::new_with_ctx(ctx, amp_packet.messages.clone()); execute_env.amp_ctx = Some(new_amp_packet); } @@ -209,11 +210,12 @@ pub fn do_ibc_packet_receive( match username_addr { Some(addr) => { // Add potential username to the context - let mut ctx = AMPCtx::new(addr, env.contract.address, original_sender_username); - // Add previous hops to the context - for hop in previous_hops { - ctx.add_hop(hop); - } + let ctx = AMPCtx::new_with_hops( + addr, + env.contract.address, + original_sender_username, + previous_hops, + ); let amp_packet = AMPPkt::new_with_ctx(ctx, vec![msg.clone()]); execute_env.amp_ctx = Some(amp_packet.clone()); diff --git a/packages/std/src/amp/messages.rs b/packages/std/src/amp/messages.rs index 89080c96c..54342d05d 100644 --- a/packages/std/src/amp/messages.rs +++ b/packages/std/src/amp/messages.rs @@ -240,6 +240,21 @@ impl AMPCtx { } } + pub fn new_with_hops( + origin: impl Into, + previous_sender: impl Into, + origin_username: Option, + previous_hops: Vec, + ) -> AMPCtx { + AMPCtx { + origin: origin.into(), + origin_username, + previous_sender: previous_sender.into(), + id: None, + previous_hops, + } + } + /// Gets the original sender of a message pub fn get_origin(&self) -> String { self.origin.clone() From 80e62ce6c72955a1a48e495d3a7b541ce8e987c4 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 14 Aug 2025 15:23:52 +0300 Subject: [PATCH 11/16] chore: version bump and changelog entry --- CHANGELOG.md | 1 + Cargo.lock | 2 +- contracts/os/andromeda-kernel/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb72c20f..7e2ee3827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat: Create pool with funds and withdraw from pool in osmosis socket [(#886)](https://github.com/andromedaprotocol/andromeda-core/pull/886) - feat: Osmosis Token Factory ADO [(#929)](https://github.com/andromedaprotocol/andromeda-core/pull/929) - feat: Wildcard in Permissions [(#949)](https://github.com/andromedaprotocol/andromeda-core/pull/949) +- feat: Proxy ADO [(#961)](https://github.com/andromedaprotocol/andromeda-core/pull/961) ### Changed diff --git a/Cargo.lock b/Cargo.lock index 8f1aa6593..34bf173df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,7 +607,7 @@ dependencies = [ [[package]] name = "andromeda-kernel" -version = "1.2.5-b.1" +version = "1.2.5-b.2" dependencies = [ "andromeda-std", "base64 0.22.1", diff --git a/contracts/os/andromeda-kernel/Cargo.toml b/contracts/os/andromeda-kernel/Cargo.toml index b8bb1eaf0..44fdfc277 100644 --- a/contracts/os/andromeda-kernel/Cargo.toml +++ b/contracts/os/andromeda-kernel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-kernel" -version = "1.2.5-b.1" +version = "1.2.5-b.2" authors = ["Connor Barr "] edition = "2021" rust-version = "1.65.0" From 45ba2e880b80dc5bf1c76254ac5b264d6ee0851c Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 14 Aug 2025 15:26:49 +0300 Subject: [PATCH 12/16] chore: clippy --- tests/ibc-tests/proxy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ibc-tests/proxy.rs b/tests/ibc-tests/proxy.rs index 1d30d729b..c19d81f4f 100644 --- a/tests/ibc-tests/proxy.rs +++ b/tests/ibc-tests/proxy.rs @@ -37,7 +37,7 @@ fn test_proxy_ibc() { proxy_osmosis .instantiate( &andromeda_socket::proxy::InstantiateMsg { - admins: admins, + admins, kernel_address: osmosis.aos.kernel.address().unwrap().into_string(), owner: None, }, From 77c2714ed032fcd2ab1076da690173febdc35feb Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 25 Aug 2025 10:10:17 +0300 Subject: [PATCH 13/16] fix: remove unused code, set admin as AndrAddr --- .../socket/andromeda-proxy/examples/schema.rs | 6 +- .../socket/andromeda-proxy/src/contract.rs | 40 +++---------- contracts/socket/andromeda-proxy/src/mock.rs | 16 +---- contracts/socket/andromeda-proxy/src/state.rs | 7 +-- packages/andromeda-socket/src/proxy.rs | 58 +------------------ 5 files changed, 17 insertions(+), 110 deletions(-) diff --git a/contracts/socket/andromeda-proxy/examples/schema.rs b/contracts/socket/andromeda-proxy/examples/schema.rs index ddc49b8dc..27e5e4010 100644 --- a/contracts/socket/andromeda-proxy/examples/schema.rs +++ b/contracts/socket/andromeda-proxy/examples/schema.rs @@ -1,5 +1,5 @@ -use andromeda_socket::proxy::{Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg}; -use cosmwasm_schema::{export_schema_with_title, schema_for, write_api}; +use andromeda_socket::proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; use std::env::current_dir; fn main() { @@ -10,6 +10,4 @@ fn main() { query: QueryMsg, execute: ExecuteMsg, }; - // Add this if there is a cw20 receive integration - export_schema_with_title(&schema_for!(Cw20HookMsg), &out_dir, "cw20receive"); } diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index c3c9d68ce..c13c129ae 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -1,18 +1,16 @@ -use crate::state::{authorize, ADMINS, LOCKED}; -use andromeda_socket::proxy::{ - AllLockedResponse, ExecuteMsg, InitParams, InstantiateMsg, LockedInfo, LockedResponse, QueryMsg, -}; +use crate::state::{authorize, ADMINS}; +use andromeda_socket::proxy::{ExecuteMsg, InitParams, InstantiateMsg, QueryMsg}; use andromeda_std::{ ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, ado_contract::ADOContract, amp::AndrAddr, andr_execute_fn, - common::{context::ExecuteContext, encode_binary}, + common::context::ExecuteContext, error::ContractError, os::aos_querier::AOSQuerier, }; use cosmwasm_std::{ - attr, entry_point, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + attr, entry_point, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, SubMsg, WasmMsg, }; use cw2::set_contract_version; @@ -93,7 +91,7 @@ fn send_instantiate( ctx: ExecuteContext, init_params: InitParams, message: Binary, - admin: Option, + admin: Option, label: Option, ) -> Result { authorize(&ctx)?; @@ -109,6 +107,10 @@ fn send_instantiate( } }; + let admin = match admin { + Some(admin) => Some(admin.get_raw_address(&ctx.deps.as_ref())?.into_string()), + None => None, + }; // Forward the message let sub_msg = SubMsg::reply_always( CosmosMsg::Wasm(WasmMsg::Instantiate { @@ -140,8 +142,6 @@ fn execute_modify_admins( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::Locked { cw20_addr } => encode_binary(&query_locked(deps, cw20_addr)?), - QueryMsg::AllLocked {} => encode_binary(&query_all_locked(deps)?), _ => ADOContract::default().query(deps, env, msg), } } @@ -170,25 +170,3 @@ pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result Result { - let amount = LOCKED - .may_load(deps.storage, cw20_addr)? - .unwrap_or_default(); - Ok(LockedResponse { amount }) -} - -fn query_all_locked(deps: Deps) -> Result { - let locked: Vec = LOCKED - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .filter_map(|item| { - let (cw20_addr, amount) = item.ok()?; - if !amount.is_zero() { - Some(LockedInfo { cw20_addr, amount }) - } else { - None - } - }) - .collect(); - Ok(AllLockedResponse { locked }) -} diff --git a/contracts/socket/andromeda-proxy/src/mock.rs b/contracts/socket/andromeda-proxy/src/mock.rs index 765cdcf4a..1955de949 100644 --- a/contracts/socket/andromeda-proxy/src/mock.rs +++ b/contracts/socket/andromeda-proxy/src/mock.rs @@ -1,9 +1,7 @@ #![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] use crate::contract::{execute, instantiate, query}; -use andromeda_socket::proxy::{ - AllLockedResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, -}; +use andromeda_socket::proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; use andromeda_std::ado_base::permissioning::{Permission, PermissioningMessage}; use andromeda_std::ado_base::rates::{Rate, RatesMessage}; use andromeda_std::amp::messages::AMPPkt; @@ -64,10 +62,6 @@ impl MockProxy { let msg = mock_set_permission(actors, action, permission); self.execute(app, &msg, sender, &[]) } - - pub fn query_all_locked(&self, app: &MockApp) -> AllLockedResponse { - self.query::(app, query_all_locked()) - } } pub fn mock_andromeda_osmosis_token_factory() -> Box> { @@ -103,14 +97,6 @@ pub fn mock_set_permission( }) } -pub fn mock_cw20_hook_msg(recipient: Option) -> Cw20HookMsg { - Cw20HookMsg::Lock { recipient } -} - pub fn mock_receive_packet(packet: AMPPkt) -> ExecuteMsg { ExecuteMsg::AMPReceive(packet) } - -pub fn query_all_locked() -> QueryMsg { - QueryMsg::AllLocked {} -} diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs index 609b5ba55..d87d67cf5 100644 --- a/contracts/socket/andromeda-proxy/src/state.rs +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -1,9 +1,6 @@ use andromeda_std::{common::context::ExecuteContext, error::ContractError}; -use cosmwasm_std::{ensure, Addr, Uint128}; -use cw_storage_plus::{Item, Map}; - -/// Maps cw20_address -> locked_amount -pub const LOCKED: Map = Map::new("locked"); +use cosmwasm_std::ensure; +use cw_storage_plus::Item; /// List of addresses that are allowed to operate this ADO pub const ADMINS: Item> = Item::new("admins"); diff --git a/packages/andromeda-socket/src/proxy.rs b/packages/andromeda-socket/src/proxy.rs index a6a987a21..56466bb97 100644 --- a/packages/andromeda-socket/src/proxy.rs +++ b/packages/andromeda-socket/src/proxy.rs @@ -1,12 +1,8 @@ use andromeda_std::amp::AndrAddr; -use andromeda_std::error::ContractError; use andromeda_std::os::adodb::ADOVersion; use andromeda_std::{andr_exec, andr_instantiate, andr_query}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{ - to_json_binary, Addr, Binary, Env, QuerierWrapper, QueryRequest, Uint128, WasmQuery, -}; -use cw20::{Cw20QueryMsg, TokenInfoResponse}; +use cosmwasm_std::Binary; #[andr_instantiate] #[cw_serde] @@ -27,7 +23,7 @@ pub enum ExecuteMsg { Instantiate { init_params: InitParams, message: Binary, - admin: Option, + admin: Option, label: Option, }, @@ -43,56 +39,8 @@ pub enum ExecuteMsg { }, } -#[cw_serde] -pub enum Cw20HookMsg { - /// Lock the received CW20 tokens and mint factory tokens - Lock { recipient: Option }, -} - #[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::QueryFns))] #[andr_query] #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(LockedResponse)] - Locked { cw20_addr: Addr }, - - #[returns(AllLockedResponse)] - AllLocked {}, -} - -#[cw_serde] -pub struct LockedResponse { - pub amount: Uint128, -} - -#[cw_serde] -pub struct FactoryDenomResponse { - pub denom: Option, -} - -#[cw_serde] -pub struct LockedInfo { - pub cw20_addr: Addr, - pub amount: Uint128, -} - -#[cw_serde] -pub struct AllLockedResponse { - pub locked: Vec, -} - -/// The structure of the newly created denom is: “factory/{osmosis_socket_addr}/{subdenom}}” -pub fn get_factory_denom(env: &Env, subdenom: &str) -> String { - format!("factory/{}/{}", env.contract.address, subdenom) -} - -/// Checks if an address is a cw20 contract by querying the TokenInfo which is guaranteed to error if the address is a wallet -pub fn is_cw20_contract(querier: &QuerierWrapper, address: &str) -> Result { - Ok(querier - .query::(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: address.to_string(), - msg: to_json_binary(&Cw20QueryMsg::TokenInfo {})?, - })) - .is_ok()) -} +pub enum QueryMsg {} From 18c4b7064660dd17878ad1e597151b8c50e2898f Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 25 Aug 2025 10:45:51 +0300 Subject: [PATCH 14/16] chore: linting --- contracts/socket/andromeda-proxy/src/contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index c13c129ae..ecd093198 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -141,9 +141,7 @@ fn execute_modify_admins( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { - match msg { - _ => ADOContract::default().query(deps, env, msg), - } + ADOContract::default().query(deps, env, msg) } #[cfg_attr(not(feature = "library"), entry_point)] From acd84d1214d70793eb32418e26d32ac471ead946 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 2 Sep 2025 15:04:45 +0300 Subject: [PATCH 15/16] feat: batch send --- .../socket/andromeda-proxy/src/contract.rs | 143 +++++++++++++++--- contracts/socket/andromeda-proxy/src/state.rs | 11 ++ packages/andromeda-socket/src/proxy.rs | 30 ++++ 3 files changed, 162 insertions(+), 22 deletions(-) diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs index ecd093198..8210fa2b0 100644 --- a/contracts/socket/andromeda-proxy/src/contract.rs +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -1,5 +1,10 @@ -use crate::state::{authorize, ADMINS}; -use andromeda_socket::proxy::{ExecuteMsg, InitParams, InstantiateMsg, QueryMsg}; +use crate::state::{ + authorize, get_reply_id, ADMINS, BATCH_REPLY_ID_FAIL_ON_ERROR, BATCH_REPLY_ID_IGNORE_ERROR, + REPLY_ID, +}; +use andromeda_socket::proxy::{ + ExecuteMsg, ExecutionType, InitParams, InstantiateMsg, Operation, QueryMsg, +}; use andromeda_std::{ ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, ado_contract::ADOContract, @@ -55,11 +60,12 @@ pub fn execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result send_instantiate(ctx, init_params, message, admin, label), + } => send_instantiate(&ctx, init_params, message, admin, label, None), ExecuteMsg::Execute { contract_addr, message, - } => send_execute(ctx, contract_addr, message), + } => send_execute(&ctx, contract_addr, message, None), + ExecuteMsg::BatchExecute { operations } => send_batch_execute(ctx, operations), ExecuteMsg::ModifyAdmins { admins } => execute_modify_admins(ctx, admins), _ => ADOContract::default().execute(ctx, msg), } @@ -67,34 +73,94 @@ pub fn execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result, ) -> Result { - authorize(&ctx)?; + authorize(ctx)?; + let reply_id = get_reply_id(fail_on_error); // Forward the message - let sub_msg = SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: contract_addr - .get_raw_address(&ctx.deps.as_ref())? - .into_string(), - msg: message, - funds: ctx.info.funds, - })); + let sub_msg = SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), + msg: message, + funds: ctx.info.funds.clone(), + }), + reply_id, + ); Ok(Response::default() .add_submessage(sub_msg) .add_attribute("action", "send_execute")) } -fn send_instantiate( +fn send_batch_execute( ctx: ExecuteContext, + operations: Vec, +) -> Result { + authorize(&ctx)?; + + let mut resp = Response::default(); + + for op in operations { + let result = match op.execution { + ExecutionType::Instantiate { + init_params, + message, + admin, + label, + } => send_instantiate( + &ctx, + init_params, + message, + admin, + label, + Some(op.fail_on_error), + ), + ExecutionType::Execute { + contract_addr, + message, + } => send_execute(&ctx, contract_addr, message, Some(op.fail_on_error)), + ExecutionType::Migrate { + contract_addr, + new_code_id, + migrate_msg, + } => send_migrate( + &ctx, + contract_addr, + new_code_id, + migrate_msg, + Some(op.fail_on_error), + ), + }; + + match result { + Ok(new_res) => { + resp = resp + .add_submessages(new_res.messages) + .add_attributes(new_res.attributes); + } + Err(e) if op.fail_on_error => return Err(e), + Err(_) => {} // ignore error, continue loop + } + } + + Ok(resp) +} + +fn send_instantiate( + ctx: &ExecuteContext, init_params: InitParams, message: Binary, admin: Option, label: Option, + fail_on_error: Option, ) -> Result { - authorize(&ctx)?; + authorize(ctx)?; let code_id = match init_params { InitParams::CodeId(code_id) => code_id, @@ -111,16 +177,17 @@ fn send_instantiate( Some(admin) => Some(admin.get_raw_address(&ctx.deps.as_ref())?.into_string()), None => None, }; + let reply_id = get_reply_id(fail_on_error); // Forward the message - let sub_msg = SubMsg::reply_always( + let sub_msg = SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Instantiate { admin, code_id, msg: message, - funds: ctx.info.funds, + funds: ctx.info.funds.clone(), label: label.unwrap_or("default".to_string()), }), - 1, + reply_id, ); Ok(Response::default() @@ -139,6 +206,31 @@ fn execute_modify_admins( Ok(Response::default().add_attribute("action", "modify_admins")) } +fn send_migrate( + ctx: &ExecuteContext, + contract_addr: AndrAddr, + new_code_id: u64, + migrate_msg: Binary, + fail_on_error: Option, +) -> Result { + authorize(ctx)?; + let reply_id = get_reply_id(fail_on_error); + let sub_msg = SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: contract_addr + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), + new_code_id, + msg: migrate_msg, + }), + reply_id, + ); + + Ok(Response::default() + .add_submessage(sub_msg) + .add_attribute("action", "send_migrate")) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { ADOContract::default().query(deps, env, msg) @@ -152,17 +244,24 @@ pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result Result { match msg.id { - 1 => { + REPLY_ID => { if msg.result.is_err() { Err(ContractError::Std(StdError::generic_err(format!( - "Contract instantiation error: {:?}", + "Contract error: {:?}", msg.result.unwrap_err() )))) } else { - Ok(Response::default() - .add_attributes(vec![attr("action", "contract_instantiated")])) + Ok( + Response::default() + .add_attributes(vec![attr("action", "message_sent_success")]), + ) } } + BATCH_REPLY_ID_FAIL_ON_ERROR => Err(ContractError::Std(StdError::generic_err(format!( + "Contract error: {:?}", + msg.result.unwrap_err() + )))), + BATCH_REPLY_ID_IGNORE_ERROR => Ok(Response::default()), _ => Err(ContractError::Std(StdError::generic_err( "Invalid Reply ID".to_string(), ))), diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs index d87d67cf5..265a7a819 100644 --- a/contracts/socket/andromeda-proxy/src/state.rs +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -22,3 +22,14 @@ pub(crate) fn authorize(ctx: &ExecuteContext) -> Result<(), ContractError> { ensure!(admins.contains(&sender), ContractError::Unauthorized {}); Ok(()) } + +pub(crate) const REPLY_ID: u64 = 1; +pub(crate) const BATCH_REPLY_ID_FAIL_ON_ERROR: u64 = 101; +pub(crate) const BATCH_REPLY_ID_IGNORE_ERROR: u64 = 201; +pub(crate) fn get_reply_id(fail_on_error: Option) -> u64 { + match fail_on_error { + Some(true) => BATCH_REPLY_ID_FAIL_ON_ERROR, + Some(false) => BATCH_REPLY_ID_IGNORE_ERROR, + None => REPLY_ID, + } +} diff --git a/packages/andromeda-socket/src/proxy.rs b/packages/andromeda-socket/src/proxy.rs index 56466bb97..79960a4b0 100644 --- a/packages/andromeda-socket/src/proxy.rs +++ b/packages/andromeda-socket/src/proxy.rs @@ -16,6 +16,31 @@ pub enum InitParams { AdoVersion(ADOVersion), } +#[cw_serde] +pub struct Operation { + pub execution: ExecutionType, + pub fail_on_error: bool, +} + +#[cw_serde] +pub enum ExecutionType { + Instantiate { + init_params: InitParams, + message: Binary, + admin: Option, + label: Option, + }, + Execute { + contract_addr: AndrAddr, + message: Binary, + }, + Migrate { + contract_addr: AndrAddr, + new_code_id: u64, + migrate_msg: Binary, + }, +} + #[andr_exec] #[cw_serde] #[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::ExecuteFns))] @@ -34,6 +59,11 @@ pub enum ExecuteMsg { // Funds will be native }, + #[cfg_attr(not(target_arch = "wasm32"), cw_orch(payable))] + BatchExecute { + operations: Vec, + }, + ModifyAdmins { admins: Vec, }, From 9a8f4b328b93060d5849678788e4db38a255c139 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Sun, 7 Sep 2025 15:02:08 +0300 Subject: [PATCH 16/16] fix: linting --- Cargo.lock | 2 +- tests/ibc-tests/proxy.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca03d5b5a..e2527acbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -821,7 +821,7 @@ dependencies = [ "osmosis-std", "schemars 0.8.22", "serde", - "thiserror 2.0.12", + "thiserror 2.0.15", ] [[package]] diff --git a/tests/ibc-tests/proxy.rs b/tests/ibc-tests/proxy.rs index c19d81f4f..76712eb29 100644 --- a/tests/ibc-tests/proxy.rs +++ b/tests/ibc-tests/proxy.rs @@ -80,7 +80,8 @@ fn test_proxy_ibc() { denom: "osmo".to_string(), amount: Uint128::new(100), }], - }], + }] + .into(), default_recipient: None, lock_time: None, kernel_address: osmosis.aos.kernel.address().unwrap().into_string(),