diff --git a/Cargo.lock b/Cargo.lock index 37e708eb..cf755476 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,6 +340,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw2", + "cw20", "getrandom", "hex", "schemars 0.8.12", @@ -416,6 +417,19 @@ dependencies = [ "serde", ] +[[package]] +name = "cw20" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011c45920f8200bd5d32d4fe52502506f64f2f75651ab408054d4cfc75ca3a9b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars 0.8.12", + "serde", +] + [[package]] name = "debug_print" version = "1.0.0" diff --git a/contracts/cosmwasm-vm/cw-intents-v1/Cargo.toml b/contracts/cosmwasm-vm/cw-intents-v1/Cargo.toml index ab5ccd85..865ac971 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/Cargo.toml +++ b/contracts/cosmwasm-vm/cw-intents-v1/Cargo.toml @@ -38,6 +38,7 @@ thiserror = { workspace=true} common ={ git = "https://github.com/icon-project/IBC-Integration.git",branch="main" } serde-json-wasm = {workspace=true} hex="*" +cw20="*" [dev-dependencies] cosmwasm = "0.7.2" diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/contract.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/contract.rs index 7a4553cd..a788edf6 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/src/contract.rs +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/contract.rs @@ -1,6 +1,13 @@ use std::str::from_utf8; -use cosmwasm_std::Addr; +use crate::events::create_swap_order_event; +use common::{ + rlp::{self, Decodable, Encodable}, + utils::keccak256, +}; +use cosmwasm_std::{to_binary, Addr, BankMsg, Coin}; +use cw20::Cw20ExecuteMsg; +use events::create_send_message_event; use super::*; @@ -24,16 +31,312 @@ impl<'a> CwIntentV1Service<'a> { Ok(Response::new()) } - pub fn swap(&self, order: SwapOrder, deps: DepsMut, env: Env, info: MessageInfo) {} + pub fn swap( + &self, + order: SwapOrder, + deps: DepsMut, + env: Env, + info: MessageInfo, + ) -> Result { + let event = create_swap_order_event(&order); + self.set_order(deps.storage, order.id, &order)?; + let mut res = self.receive_payment(deps.as_ref(), env, info, order.amount, order.token)?; + res = res.add_event(event); + Ok(res) + } + + pub fn fill( + &self, + order: SwapOrder, + fill_amount: u128, + solver: String, + deps: DepsMut, + env: Env, + info: MessageInfo, + ) -> Result { + let stored = self.get_order(deps.storage, order.id)?; + let order_bytes = order.rlp_bytes(); + let order_hash = keccak256(&order_bytes); + if keccak256(&stored.rlp_bytes()) != order_hash { + return Err(ContractError::DecodeError { + error: "Hash Mismatch".to_string(), + }); + } + + if self.is_order_finished(deps.storage, &order_hash) { + return Err(ContractError::OrderAlreadyComplete); + } + + let mut remaining_amount = self + .get_pending_fill(deps.storage, &order_hash) + .unwrap_or(order.amount); + + let payout = (order.amount * fill_amount) / order.min_receive; + + if payout > remaining_amount { + return Err(ContractError::PayoutGreaterThanRemaining); + } + + remaining_amount = remaining_amount - payout; + + let mut close_order = false; + if remaining_amount == 0 { + self.remove_pending_fill(deps.storage, &order_hash); + self.set_order_finished(deps.storage, &order_hash, true)?; + close_order = true; + } else { + self.set_pending_fill(deps.storage, &order_hash, remaining_amount)?; + } - pub fn fill(&self, order: SwapOrder, deps: DepsMut, env: Env, info: MessageInfo) {} + let protocol_fee = self.get_protocol_fee(deps.storage).unwrap_or(0); + let fee = (fill_amount * protocol_fee as u128) / 10000; + let fee_handler = self.get_fee_handler(deps.storage)?; - pub fn receive_msg(&self, src_network: String, conn_sn: u128, order_msg: OrderMsg) {} + let fee_transfer = self.try_transfer( + deps.as_ref(), + env.contract.address.to_string(), + fee_handler.to_string(), + fee, + &order.to_token, + ); + + let receiver_transfer = self.try_transfer( + deps.as_ref(), + env.contract.address.to_string(), + order.destination_address.clone(), + (fill_amount - fee), + &order.to_token, + ); + + let order_fill = OrderFill { + id: order.id, + order_bytes: order.rlp_bytes().to_vec(), + solver_address: solver, + amount: payout, + closed: close_order, + }; + let mut response = Response::new(); + + if order.src_nid == order.dst_nid { + response = self.resolve_fill(deps, env, order.src_nid, order_fill)?; + } else { + let msg = OrderMsg { + msg_type: ORDER_FILL, + message: order_fill.rlp_bytes().to_vec(), + }; + let sn = self.get_next_conn_sn(deps.storage)?; + let event = create_send_message_event(order.src_nid, sn, msg.rlp_bytes().to_vec()); + response = response.add_event(event); + } + response = response.add_messages(vec![fee_transfer, receiver_transfer]); + Ok(response) + } + + pub fn receive_msg( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + src_network: String, + conn_sn: u128, + order_msg: OrderMsg, + ) -> Result { + if self.have_received(deps.storage, (src_network.clone(), conn_sn)) { + return Err(ContractError::MessageAlreadyReceived); + } + self.set_received(deps.storage, (src_network.clone(), conn_sn), true)?; + match order_msg.msg_type { + ORDER_FILL => { + let fill = rlp::decode::(&order_msg.message).map_err(|e| { + ContractError::DecodeError { + error: e.to_string(), + } + })?; + return self.resolve_fill(deps, env, src_network, fill); + } + ORDER_CANCEL => { + let cancel = rlp::decode::(&order_msg.message).map_err(|e| { + ContractError::DecodeError { + error: e.to_string(), + } + })?; + return self.resolve_cancel(deps, src_network, cancel.order_bytes); + } + _ => Err(ContractError::InvalidMessageType), + } + } + + fn resolve_fill( + &self, + deps: DepsMut, + env: Env, + src_nid: String, + fill: OrderFill, + ) -> Result { + let order = self.get_order(deps.storage, fill.id)?; + if fill.order_bytes != order.rlp_bytes().to_vec() { + return Err(ContractError::InvalidFillOrder); + } + + if order.dst_nid != src_nid { + return Err(ContractError::InvalidFillOrder); + } + let mut response = Response::new(); + if fill.closed == true { + self.remove_order(deps.storage, order.id); + } + let transfer = self.try_transfer( + deps.as_ref(), + env.contract.address.to_string(), + fill.solver_address, + fill.amount, + &order.token, + ); + response = response.add_message(transfer); + Ok(response) + } + + fn resolve_cancel( + &self, + deps: DepsMut, + src_nid: String, + order_bytes: Vec, + ) -> Result { + let order_hash = keccak256(&order_bytes); + if self.is_order_finished(deps.storage, &order_hash) { + return Err(ContractError::OrderAlreadyComplete); + } + let mut response = Response::new(); + let order: SwapOrder = + rlp::decode(&order_bytes).map_err(|e| ContractError::DecodeError { + error: e.to_string(), + })?; + if order.src_nid != src_nid { + return Err(ContractError::InvalidCancellation); + } + + let remaining_amount = self + .get_pending_fill(deps.storage, &order_hash) + .unwrap_or(order.amount); + self.remove_pending_fill(deps.storage, &order_hash); + self.set_order_finished(deps.storage, &order_hash, true)?; + let fill = OrderFill { + id: order.id, + order_bytes, + solver_address: order.creator, + amount: remaining_amount, + closed: true, + }; + let msg = OrderMsg { + msg_type: ORDER_CANCEL, + message: fill.rlp_bytes().to_vec(), + }; + let conn_sn = self.get_next_conn_sn(deps.storage)?; + let event = create_send_message_event(order.src_nid, conn_sn, msg.rlp_bytes().to_vec()); + response = response.add_event(event); + + Ok(response) + } + + fn receive_payment( + &self, + deps: Deps, + env: Env, + info: MessageInfo, + amount: u128, + denom: String, + ) -> Result { + let mut response = Response::new(); + if self.is_native(deps, &denom) { + let sum: u128 = info.funds.into_iter().fold(0, |mut a, c| { + if c.denom == denom { + a = a + Into::::into(c.amount); + } + return a; + }); + if sum < amount { + return Err(ContractError::InsufficientFunds); + } + } else { + let msg = self.token_transfer( + denom, + info.sender.to_string(), + env.contract.address.to_string(), + amount, + ); + response = response.add_message(msg); + } + + Ok(response) + } + + fn token_transfer( + &self, + token_address: String, + from: String, + to: String, + amount: u128, + ) -> CosmosMsg { + let transfer_msg = Cw20ExecuteMsg::TransferFrom { + owner: from, + recipient: to, + amount: amount.into(), + }; + + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token_address, + msg: to_json_binary(&transfer_msg).unwrap(), + funds: vec![], + }) + } + + fn try_transfer( + &self, + deps: Deps, + from: String, + to: String, + amount: u128, + denom: &String, + ) -> CosmosMsg { + if self.is_native(deps, &denom) { + let msg = BankMsg::Send { + to_address: deps.api.addr_validate(&to).unwrap().to_string(), + amount: vec![Coin { + denom: denom.to_string(), + amount: amount.into(), + }], + }; + CosmosMsg::Bank(msg) + } else { + self.token_transfer(denom.to_string(), from, to, amount) + } + } + + pub fn is_contract(&self, deps: Deps, address: &Addr) -> bool { + deps.querier + .query_wasm_contract_info(address) + .map(|r| true) + .unwrap_or(false) + } + + pub fn is_native(&self, deps: Deps, denom: &String) -> bool { + if let Some(addr) = deps.api.addr_validate(denom).ok() { + return self.is_contract(deps, &addr); + } + return false; + } pub fn get_next_deposit_id(&self, storage: &mut dyn Storage) -> StdResult { - let id = self.get_deposit_id(storage)?; + let id = self.get_deposit_id(storage); let new_id = id + 1; self.set_deposit_id(storage, new_id)?; Ok(new_id) } + + pub fn get_next_conn_sn(&self, storage: &mut dyn Storage) -> StdResult { + let id = self.get_conn_sn(storage); + let new_id = id + 1; + self.set_conn_sn(storage, new_id)?; + Ok(new_id) + } } diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/errors.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/errors.rs index c6e4eb97..7b8498ec 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/src/errors.rs +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/errors.rs @@ -10,14 +10,19 @@ pub enum ContractError { DecodeError { error: String }, #[error("RollBackMessageMismatch {sequence}")] RollBackMismatch { sequence: u64 }, - #[error("RevertFromDAPP")] - RevertFromDAPP, - #[error("ModuleAddressNotFound")] - ModuleAddressNotFound, - #[error("MisiingRollBack {sequence}")] - MisiingRollBack { sequence: u64 }, - #[error("Connection Not Found {network_id}")] - ConnectionNotFound { network_id: String }, - #[error("Invalid Address {address}")] - InvalidAddress { address: String }, + #[error("InsufficientFunds")] + InsufficientFunds, + #[error("OrderAlreadyComplete")] + OrderAlreadyComplete, + + #[error("PayoutGreaterThanRemaining")] + PayoutGreaterThanRemaining, + #[error("InvalidFillOrder")] + InvalidFillOrder, + #[error("InvalidCancellation")] + InvalidCancellation, + #[error("InvalidMessageType")] + InvalidMessageType, + #[error("MessageAlreadyReceived")] + MessageAlreadyReceived, } diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/events.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/events.rs new file mode 100644 index 00000000..4294d5d6 --- /dev/null +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/events.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::Event; + +use crate::{OrderFill, SwapOrder}; + +pub fn create_swap_order_event(order: &SwapOrder) -> Event { + Event::new("SwapOrder") + .add_attribute("id", order.id.to_string()) + .add_attribute("amount", order.amount.to_string()) + .add_attribute("creator", order.creator.to_string()) + .add_attribute("data", hex::encode(&order.data)) + .add_attribute("destination_address", order.destination_address.to_string()) + .add_attribute("dst_nid", order.dst_nid.to_string()) + .add_attribute("emitter", order.emitter.to_string()) + .add_attribute("min_receive", order.min_receive.to_string()) + .add_attribute("src_nid", order.src_nid.to_string()) + .add_attribute("to_token", order.to_token.to_string()) + .add_attribute("token", order.token.to_string()) +} + +pub fn create_order_fill_event(fill: &OrderFill) -> Event { + Event::new("OrderFill") + .add_attribute("id", fill.id.to_string()) + .add_attribute("amount", fill.amount.to_string()) + .add_attribute("solver_address", fill.solver_address.to_string()) + .add_attribute("order_bytes", hex::encode(&fill.order_bytes)) + .add_attribute("closed", fill.closed.to_string()) +} + +pub fn create_send_message_event(nid: String, conn_sn: u128, msg: Vec) -> Event { + Event::new("SendMessage") + .add_attribute("sn", conn_sn.to_string()) + .add_attribute("to", nid.to_string()) + .add_attribute("msg", hex::encode(msg)) +} diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/lib.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/lib.rs index f1ca7758..dd5cf79b 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/src/lib.rs +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/lib.rs @@ -1,9 +1,11 @@ pub mod contract; pub mod errors; +pub mod events; pub mod msg; pub mod state; pub mod types; +use common::rlp; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ entry_point, to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, @@ -63,8 +65,7 @@ pub fn execute( min_receive, data, ); - call_service.swap(order, deps, env, info); - Ok(Response::new()) + return call_service.swap(order, deps, env, info); } ExecuteMsg::Fill { id, @@ -80,12 +81,33 @@ pub fn execute( data, fill_amount, solver_address, - } => Ok(Response::new()), + } => { + let order = SwapOrder { + id, + emitter, + src_nid, + dst_nid, + creator, + destination_address, + token, + amount, + to_token, + min_receive, + data, + }; + return call_service.fill(order, fill_amount, solver_address, deps, env, info); + } ExecuteMsg::RecvMessage { src_network, conn_sn, msg, - } => Ok(Response::new()), + } => { + let order_msg = + rlp::decode::(&msg).map_err(|e| ContractError::DecodeError { + error: e.to_string(), + })?; + return call_service.receive_msg(deps, env, info, src_network, conn_sn, order_msg); + } } } @@ -96,5 +118,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::GetOrder { id } => { to_json_binary(&call_service.get_order(deps.storage, id).unwrap()) } + QueryMsg::GetDepositId {} => to_json_binary(&call_service.get_deposit_id(deps.storage)), } } diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/msg.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/msg.rs index 58561780..54c9764e 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/src/msg.rs +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/msg.rs @@ -29,7 +29,7 @@ pub enum ExecuteMsg { RecvMessage { src_network: String, conn_sn: u128, - msg: String, + msg: Vec, }, } @@ -41,4 +41,7 @@ pub enum ExecuteMsg { pub enum QueryMsg { #[returns(u64)] GetOrder { id: u128 }, + + #[returns(u64)] + GetDepositId {}, } diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/state.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/state.rs index d983bf5a..7628231a 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/src/state.rs +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/state.rs @@ -12,6 +12,8 @@ pub struct CwIntentV1Service<'a> { orders: Map<'a, u128, SwapOrder>, pending_fills: Map<'a, Vec, u128>, finished_orders: Map<'a, Vec, bool>, + conn_sn: Item<'a, u128>, + receipts: Map<'a, (String, u128), bool>, } impl<'a> Default for CwIntentV1Service<'a> { @@ -30,11 +32,13 @@ impl<'a> CwIntentV1Service<'a> { orders: Map::new(StorageKey::Orders.as_str()), pending_fills: Map::new(StorageKey::PendingFills.as_str()), finished_orders: Map::new(StorageKey::FinishedOrders.as_str()), + conn_sn: Item::new(StorageKey::ConnectionSN.as_str()), + receipts: Map::new(StorageKey::Receipts.as_str()), } } - pub fn get_deposit_id(&self, storage: &dyn Storage) -> StdResult { - self.deposit_id.load(storage) + pub fn get_deposit_id(&self, storage: &dyn Storage) -> u128 { + self.deposit_id.load(storage).unwrap_or(0) } pub fn get_nid(&self, storage: &dyn Storage) -> StdResult { @@ -49,25 +53,37 @@ impl<'a> CwIntentV1Service<'a> { self.fee_handler.load(storage) } - pub fn get_order(&self, storage: &dyn Storage, key: u128) -> StdResult> { - self.orders.may_load(storage, key) + pub fn get_order(&self, storage: &dyn Storage, key: u128) -> StdResult { + self.orders.load(storage, key) } pub fn get_pending_fill(&self, storage: &dyn Storage, key: &[u8]) -> Option { self.pending_fills.load(storage, key.to_vec()).ok() } + pub fn get_conn_sn(&self, storage: &dyn Storage) -> u128 { + self.conn_sn.load(storage).unwrap_or(0) + } + pub fn is_order_finished(&self, storage: &dyn Storage, key: &[u8]) -> bool { self.finished_orders .load(storage, key.to_vec()) .unwrap_or(false) } + pub fn have_received(&self, storage: &dyn Storage, key: (String, u128)) -> bool { + self.receipts.load(storage, key).unwrap_or(false) + } + // Setters pub fn set_deposit_id(&self, storage: &mut dyn Storage, value: u128) -> StdResult<()> { self.deposit_id.save(storage, &value) } + pub fn set_conn_sn(&self, storage: &mut dyn Storage, value: u128) -> StdResult<()> { + self.conn_sn.save(storage, &value) + } + pub fn set_nid(&self, storage: &mut dyn Storage, value: String) -> StdResult<()> { self.nid.save(storage, &value) } @@ -98,6 +114,14 @@ impl<'a> CwIntentV1Service<'a> { self.pending_fills.save(storage, key.to_vec(), &value) } + pub fn remove_pending_fill(&self, storage: &mut dyn Storage, key: &[u8]) { + self.pending_fills.remove(storage, key.to_vec()) + } + + pub fn remove_order(&self, storage: &mut dyn Storage, key: u128) { + self.orders.remove(storage, key) + } + pub fn set_order_finished( &self, storage: &mut dyn Storage, @@ -106,4 +130,13 @@ impl<'a> CwIntentV1Service<'a> { ) -> StdResult<()> { self.finished_orders.save(storage, key.to_vec(), &value) } + + pub fn set_received( + &self, + storage: &mut dyn Storage, + key: (String, u128), + val: bool, + ) -> StdResult<()> { + self.receipts.save(storage, key, &val) + } } diff --git a/contracts/cosmwasm-vm/cw-intents-v1/src/types.rs b/contracts/cosmwasm-vm/cw-intents-v1/src/types.rs index 83ca95c7..e01dfd48 100644 --- a/contracts/cosmwasm-vm/cw-intents-v1/src/types.rs +++ b/contracts/cosmwasm-vm/cw-intents-v1/src/types.rs @@ -1,8 +1,11 @@ use super::*; use common::rlp::{self, Decodable, DecoderError, Encodable, RlpStream}; +use cosmwasm_std::Addr; +use cw_storage_plus::{Key, KeyDeserialize, PrimaryKey}; +use serde::Serialize; -const ORDER_FILL: u8 = 1; -const ORDER_CANCEL: u8 = 2; +pub const ORDER_FILL: u8 = 1; +pub const ORDER_CANCEL: u8 = 2; #[cw_serde] pub struct InstantiateMsg { pub fee_handler: String, @@ -20,6 +23,8 @@ pub enum StorageKey { PendingOrderAmount, PendingFills, FinishedOrders, + ConnectionSN, + Receipts, } impl StorageKey { @@ -33,6 +38,8 @@ impl StorageKey { StorageKey::PendingOrderAmount => "pending_order_amount", StorageKey::PendingFills => "pending_fills", StorageKey::FinishedOrders => "finished_orders", + StorageKey::ConnectionSN => "conn_sn", + StorageKey::Receipts => "receipts", } } } @@ -82,9 +89,44 @@ impl SwapOrder { } } +impl Encodable for SwapOrder { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(11); + s.append(&self.id); + s.append(&self.emitter); + s.append(&self.src_nid); + s.append(&self.dst_nid); + s.append(&self.creator); + s.append(&self.destination_address); + s.append(&self.token); + s.append(&self.amount); + s.append(&self.to_token); + s.append(&self.min_receive); + s.append(&self.data); + } +} + +impl Decodable for SwapOrder { + fn decode(rlp: &rlp::Rlp) -> Result { + Ok(SwapOrder { + id: rlp.val_at(0)?, + emitter: rlp.val_at(1)?, + src_nid: rlp.val_at(2)?, + dst_nid: rlp.val_at(3)?, + creator: rlp.val_at(4)?, + destination_address: rlp.val_at(5)?, + token: rlp.val_at(6)?, + amount: rlp.val_at(7)?, + to_token: rlp.val_at(8)?, + min_receive: rlp.val_at(9)?, + data: rlp.val_at(10)?, + }) + } +} + pub struct OrderMsg { - msg_type: u8, - message: Vec, + pub msg_type: u8, + pub message: Vec, } impl Encodable for OrderMsg { @@ -104,11 +146,11 @@ impl Decodable for OrderMsg { } pub struct OrderFill { - id: u128, - order_bytes: Vec, - solver_address: String, - amount: u128, - closed: bool, + pub id: u128, + pub order_bytes: Vec, + pub solver_address: String, + pub amount: u128, + pub closed: bool, } impl Encodable for OrderFill { @@ -135,7 +177,7 @@ impl Decodable for OrderFill { } pub struct OrderCancel { - order_bytes: Vec, + pub order_bytes: Vec, } impl Encodable for OrderCancel {