diff --git a/Cargo.lock b/Cargo.lock index d6c6ff402..1d028b6bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "andromeda-std" -version = "1.4.0" +version = "1.5.0" dependencies = [ "andromeda-macros", "cosmwasm-schema 1.5.8", diff --git a/contracts/modules/andromeda-rates/src/contract.rs b/contracts/modules/andromeda-rates/src/contract.rs index e6a5883dd..c59b4aaac 100644 --- a/contracts/modules/andromeda-rates/src/contract.rs +++ b/contracts/modules/andromeda-rates/src/contract.rs @@ -88,8 +88,7 @@ fn execute_set_rate( ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, ContractError::Unauthorized {} ); - // Validate the local rate's value - rate.value.validate(deps.as_ref())?; + rate.validate(deps.as_ref())?; RATES.save(deps.storage, &action, &rate)?; diff --git a/contracts/os/andromeda-kernel/src/execute.rs b/contracts/os/andromeda-kernel/src/execute.rs index dedaebcec..25b329572 100644 --- a/contracts/os/andromeda-kernel/src/execute.rs +++ b/contracts/os/andromeda-kernel/src/execute.rs @@ -774,7 +774,6 @@ impl MsgHandler { gas_limit: None, reply_on: cosmwasm_std::ReplyOn::Always, }); - Ok(resp .add_attribute(format!("method:{sequence}"), "execute_transfer_funds") .add_attribute(format!("channel:{sequence}"), channel) diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 1d41c2229..54072c368 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-std" -version = "1.4.0" +version = "1.5.0" edition = "2021" rust-version = "1.75.0" description = "The standard library for creating an Andromeda Digital Object" diff --git a/packages/std/src/ado_base/rates.rs b/packages/std/src/ado_base/rates.rs index 8344f3e9d..0c69bcc46 100644 --- a/packages/std/src/ado_base/rates.rs +++ b/packages/std/src/ado_base/rates.rs @@ -1,14 +1,17 @@ use crate::{ ado_contract::ADOContract, - amp::{AndrAddr, Recipient}, + amp::{ + messages::{AMPMsg, AMPMsgConfig}, + AndrAddr, Recipient, + }, common::{deduct_funds, denom::validate_native_denom, Funds}, error::ContractError, os::{adodb::ADOVersion, aos_querier::AOSQuerier}, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, has_coins, to_json_binary, Coin, Decimal, Deps, Event, Fraction, QueryRequest, SubMsg, - WasmQuery, + ensure, has_coins, to_json_binary, Addr, Coin, Decimal, Deps, Event, Fraction, QueryRequest, + ReplyOn, SubMsg, WasmMsg, WasmQuery, }; use cw20::{Cw20Coin, Cw20QueryMsg, TokenInfoResponse}; @@ -80,6 +83,20 @@ pub enum LocalRateValue { Flat(Coin), } impl LocalRateValue { + /// Used to see if the denom is potentially a cw20 address, if it is, it cannot be paired with a cross-chain recipient + pub fn is_valid_address(&self, deps: Deps) -> Result { + match self { + LocalRateValue::Flat(coin) => { + let denom = coin.denom.clone(); + let is_valid_address = deps.api.addr_validate(denom.as_str()); + match is_valid_address { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } + LocalRateValue::Percent(_) => Ok(false), + } + } pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { match self { // If it's a coin, make sure it's non-zero @@ -131,6 +148,18 @@ pub struct LocalRate { pub value: LocalRateValue, pub description: Option, } +impl LocalRate { + pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { + if self.recipient.is_cross_chain() { + ensure!( + !self.value.is_valid_address(deps)?, + ContractError::InvalidCw20CrossChainRate {} + ); + } + self.value.validate(deps)?; + Ok(()) + } +} // Created this because of the very complex return value warning. type LocalRateResponse = (Vec, Vec, Vec); @@ -160,24 +189,54 @@ impl LocalRate { event = event.add_attribute( "payment", PaymentAttribute { - receiver: self.recipient.address.get_raw_address(&deps)?.to_string(), + receiver: self + .recipient + .address + .get_raw_address(&deps) + .unwrap_or(Addr::unchecked(self.recipient.address.to_string())) + .to_string(), amount: fee.clone(), } .to_string(), ); - - let msg = if is_native { - self.recipient - .generate_direct_msg(&deps, vec![fee.clone()])? - } else { - self.recipient.generate_msg_cw20( - &deps, - Cw20Coin { - amount: fee.amount, - address: fee.denom.to_string(), + let msg = if self.recipient.is_cross_chain() { + ensure!(is_native, ContractError::InvalidCw20CrossChainRate {}); + // Create a cross chain message to be sent to the kernel + let kernel_address = ADOContract::default().get_kernel_address(deps.storage)?; + let kernel_msg = crate::os::kernel::ExecuteMsg::Send { + message: AMPMsg { + recipient: self.recipient.address.clone(), + message: self.recipient.msg.clone().unwrap_or_default(), + funds: vec![fee.clone()], + config: AMPMsgConfig { + reply_on: ReplyOn::Always, + exit_at_error: false, + gas_limit: None, + direct: true, + ibc_config: None, + }, }, - )? + }; + SubMsg::new(WasmMsg::Execute { + contract_addr: kernel_address.to_string(), + msg: to_json_binary(&kernel_msg)?, + funds: vec![fee.clone()], + }) + } else { + if is_native { + self.recipient + .generate_direct_msg(&deps, vec![fee.clone()])? + } else { + self.recipient.generate_msg_cw20( + &deps, + Cw20Coin { + amount: fee.amount, + address: fee.denom.to_string(), + }, + )? + } }; + msgs.push(msg); events.push(event); @@ -215,8 +274,7 @@ impl Rate { } } Rate::Local(local_rate) => { - // Validate the local rate value - local_rate.value.validate(deps)?; + local_rate.validate(deps)?; Ok(()) } } diff --git a/packages/std/src/amp/recipient.rs b/packages/std/src/amp/recipient.rs index 8797cbcbd..520d306fa 100644 --- a/packages/std/src/amp/recipient.rs +++ b/packages/std/src/amp/recipient.rs @@ -58,6 +58,14 @@ impl Recipient { self.msg.clone() } + pub fn is_cross_chain(&self) -> bool { + let protocol = self.address.get_protocol(); + match protocol { + Some("ibc") => true, + _ => false, + } + } + /// Generates a direct sub message for the given recipient. pub fn generate_direct_msg( &self, diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 2d652287f..583c5f8a9 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -61,6 +61,9 @@ pub enum ContractError { #[error("NoDenomInfoProvided")] NoDenomInfoProvided {}, + #[error("Cannot assign cw20 rate to cross-chain recipient")] + InvalidCw20CrossChainRate {}, + #[error("InvalidAmount: {msg}")] InvalidAmount { msg: String }, diff --git a/tests-integration/tests/kernel_orch.rs b/tests-integration/tests/kernel_orch.rs index 6a35c0be6..8217e9f04 100644 --- a/tests-integration/tests/kernel_orch.rs +++ b/tests-integration/tests/kernel_orch.rs @@ -1,4 +1,5 @@ use andromeda_adodb::ADODBContract; +use andromeda_auction::{mock::mock_start_auction, AuctionContract}; use andromeda_counter::CounterContract; use andromeda_data_storage::counter::{ CounterRestriction, ExecuteMsg as CounterExecuteMsg, GetCurrentAmountResponse, @@ -9,14 +10,17 @@ use andromeda_finance::splitter::{ AddressPercent, ExecuteMsg as SplitterExecuteMsg, InstantiateMsg as SplitterInstantiateMsg, }; +use andromeda_cw721::CW721Contract; use andromeda_kernel::KernelContract; +use andromeda_non_fungible_tokens::cw721::TokenExtension; use andromeda_splitter::SplitterContract; use andromeda_std::{ + ado_base::rates::{LocalRate, LocalRateType, LocalRateValue, PercentRate, Rate, RatesMessage}, amp::{ messages::{AMPMsg, AMPMsgConfig}, AndrAddr, Recipient, }, - common::Milliseconds, + common::{denom::Asset, expiration::Expiry, Milliseconds}, os::{ self, kernel::{AcknowledgementMsg, ExecuteMsg, InstantiateMsg, SendMessageWithFundsResponse}, @@ -24,7 +28,7 @@ use andromeda_std::{ }; use andromeda_vfs::VFSContract; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Decimal, IbcAcknowledgement, IbcEndpoint, IbcPacket, + coin, to_json_binary, Addr, Binary, Decimal, IbcAcknowledgement, IbcEndpoint, IbcPacket, IbcPacketAckMsg, IbcTimeout, Timestamp, Uint128, }; use cw_orch::prelude::*; @@ -586,6 +590,7 @@ fn test_kernel_ibc_execute_only_multi_hop() { fn test_kernel_ibc_funds_only() { // Here `juno-1` is the chain-id and `juno` is the address prefix for this chain let sender = Addr::unchecked("sender_for_all_chains").into_string(); + let buyer = Addr::unchecked("buyer").into_string(); let interchain = MockInterchainEnv::new(vec![ ("juno", &sender), @@ -596,12 +601,17 @@ fn test_kernel_ibc_funds_only() { let juno = interchain.get_chain("juno").unwrap(); let osmosis = interchain.get_chain("osmosis").unwrap(); - juno.set_balance(sender.clone(), vec![Coin::new(100000000000000, "juno")]) .unwrap(); + juno.set_balance(buyer.clone(), vec![Coin::new(100000000000000, "juno")]) + .unwrap(); let kernel_juno = KernelContract::new(juno.clone()); let vfs_juno = VFSContract::new(juno.clone()); + let adodb_juno = ADODBContract::new(juno.clone()); + let economics_juno = EconomicsContract::new(juno.clone()); + let mut auction_juno = AuctionContract::new(juno.clone()); + let cw721_juno = CW721Contract::new(juno.clone()); let kernel_osmosis = KernelContract::new(osmosis.clone()); let counter_osmosis = CounterContract::new(osmosis.clone()); let vfs_osmosis = VFSContract::new(osmosis.clone()); @@ -610,6 +620,11 @@ fn test_kernel_ibc_funds_only() { kernel_juno.upload().unwrap(); vfs_juno.upload().unwrap(); + adodb_juno.upload().unwrap(); + economics_juno.upload().unwrap(); + auction_juno.upload().unwrap(); + cw721_juno.upload().unwrap(); + kernel_osmosis.upload().unwrap(); counter_osmosis.upload().unwrap(); vfs_osmosis.upload().unwrap(); @@ -703,6 +718,51 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); + adodb_juno + .instantiate( + &os::adodb::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + adodb_juno + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 4, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + economics_juno + .instantiate( + &os::economics::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "economics".to_string(), + value: economics_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + adodb_osmosis .instantiate( &os::adodb::InstantiateMsg { @@ -727,6 +787,19 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); + adodb_osmosis + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 6, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + kernel_juno .execute( &ExecuteMsg::UpsertKeyAddress { @@ -737,6 +810,16 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "adodb".to_string(), + value: adodb_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + kernel_osmosis .execute( &ExecuteMsg::UpsertKeyAddress { @@ -782,6 +865,10 @@ fn test_kernel_ibc_funds_only() { .unwrap(); let recipient = "osmo1qzskhrca90qy2yjjxqzq4yajy842x7c50xq33d"; + println!( + "osmosis kernel address: {}", + kernel_osmosis.address().unwrap() + ); let kernel_juno_send_request = kernel_juno .execute( @@ -836,7 +923,7 @@ fn test_kernel_ibc_funds_only() { .execute( &ExecuteMsg::UpsertKeyAddress { key: "trigger_key".to_string(), - value: sender, + value: sender.clone(), }, None, ) @@ -892,6 +979,182 @@ fn test_kernel_ibc_funds_only() { // There was a decode error or the packet timed out // Else the packet timed-out, you may have a relayer error or something is wrong in your application }; + + // Set up cross chain rates recipient + auction_juno + .instantiate( + &andromeda_non_fungible_tokens::auction::InstantiateMsg { + authorized_token_addresses: None, + authorized_cw20_addresses: None, + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + cw721_juno + .instantiate( + &andromeda_non_fungible_tokens::cw721::InstantiateMsg { + name: "test tokens".to_string(), + symbol: "TT".to_string(), + minter: AndrAddr::from_string(sender.clone()), + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + auction_juno + .execute( + &andromeda_non_fungible_tokens::auction::ExecuteMsg::Rates(RatesMessage::SetRate { + action: "Claim".to_string(), + rate: Rate::Local(LocalRate { + rate_type: LocalRateType::Deductive, + recipient: Recipient::new( + AndrAddr::from_string(format!("ibc://osmosis/{}", recipient)), + None, + ), + value: LocalRateValue::Percent(PercentRate { + percent: Decimal::percent(50), + }), + description: None, + }), + }), + None, + ) + .unwrap(); + + cw721_juno + .execute( + &andromeda_non_fungible_tokens::cw721::ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: sender.clone(), + token_uri: None, + extension: TokenExtension::default(), + }, + None, + ) + .unwrap(); + + let start_time = Milliseconds::from_nanos(juno.block_info().unwrap().time.nanos()); + let receive_msg = mock_start_auction( + None, + Expiry::AtTime(start_time.plus_milliseconds(Milliseconds(10000))), + None, + Asset::NativeToken("juno".to_string()), + None, + None, + None, + None, + ); + cw721_juno + .execute( + &andromeda_non_fungible_tokens::cw721::ExecuteMsg::SendNft { + contract: AndrAddr::from_string(auction_juno.address().unwrap()), + token_id: "1".to_string(), + msg: to_json_binary(&receive_msg).unwrap(), + }, + None, + ) + .unwrap(); + juno.wait_seconds(1).unwrap(); + + auction_juno.set_sender(&Addr::unchecked(buyer.clone())); + auction_juno + .execute( + &andromeda_non_fungible_tokens::auction::ExecuteMsg::PlaceBid { + token_id: "1".to_string(), + token_address: cw721_juno.address().unwrap().into_string(), + }, + Some(&[coin(50, "juno")]), + ) + .unwrap(); + juno.next_block().unwrap(); + juno.next_block().unwrap(); + + // Claim + let claim_request = auction_juno + .execute( + &andromeda_non_fungible_tokens::auction::ExecuteMsg::Claim { + token_id: "1".to_string(), + token_address: cw721_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + let packet_lifetime = interchain.await_packets("juno", claim_request).unwrap(); + + // For testing a successful outcome of the first packet sent out in the tx, you can use: + if let IbcPacketOutcome::Success { .. } = &packet_lifetime.packets[0].outcome { + // Packet has been successfully acknowledged and decoded, the transaction has gone through correctly + + // Check recipient balance after trigger execute msg + let balances = osmosis + .query_all_balances(kernel_osmosis.address().unwrap()) + .unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, ibc_denom); + assert_eq!(balances[0].amount.u128(), 25); + } else { + panic!("packet timed out"); + // There was a decode error or the packet timed out + // Else the packet timed-out, you may have a relayer error or something is wrong in your application + }; + + // Construct an Execute msg from the kernel on juno inteded for the splitter on osmosis + let kernel_juno_trigger_request = kernel_juno + .execute( + &ExecuteMsg::TriggerRelay { + packet_sequence: "2".to_string(), + packet_ack_msg: IbcPacketAckMsg::new( + IbcAcknowledgement::new( + to_json_binary(&AcknowledgementMsg::::Ok( + SendMessageWithFundsResponse {}, + )) + .unwrap(), + ), + IbcPacket::new( + Binary::default(), + IbcEndpoint { + port_id: "port_id".to_string(), + channel_id: "channel_id".to_string(), + }, + IbcEndpoint { + port_id: "port_id".to_string(), + channel_id: "channel_id".to_string(), + }, + 1, + IbcTimeout::with_timestamp(Timestamp::from_seconds(1)), + ), + Addr::unchecked("relayer"), + ), + }, + None, + ) + .unwrap(); + + let packet_lifetime = interchain + .await_packets("juno", kernel_juno_trigger_request) + .unwrap(); + + // For testing a successful outcome of the first packet sent out in the tx, you can use: + if let IbcPacketOutcome::Success { .. } = &packet_lifetime.packets[0].outcome { + // Packet has been successfully acknowledged and decoded, the transaction has gone through correctly + + // Check recipient balance after trigger execute msg + let balances = osmosis.query_all_balances(recipient).unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, ibc_denom); + assert_eq!(balances[0].amount.u128(), 100 + 25); + } else { + panic!("packet timed out"); + // There was a decode error or the packet timed out + // Else the packet timed-out, you may have a relayer error or something is wrong in your application + }; } #[test]