diff --git a/core/src/engine/inspector.rs b/core/src/engine/inspector.rs index d86d73fe..370ad37d 100644 --- a/core/src/engine/inspector.rs +++ b/core/src/engine/inspector.rs @@ -1,3 +1,4 @@ +use crate::error::Result; use crate::{ Deadline, intents::{ @@ -18,49 +19,50 @@ pub trait Inspector { sender_id: &AccountIdRef, transfer: &Transfer, intent_hash: CryptoHash, - ); + ) -> Result<()>; + fn on_token_diff( &mut self, owner_id: &AccountIdRef, token_diff: &TokenDiff, fees_collected: &Amounts, intent_hash: CryptoHash, - ); + ) -> Result<()>; fn on_ft_withdraw( &mut self, owner_id: &AccountIdRef, ft_withdraw: &FtWithdraw, intent_hash: CryptoHash, - ); + ) -> Result<()>; fn on_nft_withdraw( &mut self, owner_id: &AccountIdRef, nft_withdraw: &NftWithdraw, intent_hash: CryptoHash, - ); + ) -> Result<()>; fn on_mt_withdraw( &mut self, owner_id: &AccountIdRef, mt_withdraw: &MtWithdraw, intent_hash: CryptoHash, - ); + ) -> Result<()>; fn on_native_withdraw( &mut self, owner_id: &AccountIdRef, native_withdraw: &NativeWithdraw, intent_hash: CryptoHash, - ); + ) -> Result<()>; fn on_storage_deposit( &mut self, owner_id: &AccountIdRef, storage_deposit: &StorageDeposit, intent_hash: CryptoHash, - ); + ) -> Result<()>; fn on_intent_executed(&mut self, signer_id: &AccountIdRef, hash: CryptoHash); } diff --git a/core/src/intents/token_diff.rs b/core/src/intents/token_diff.rs index 633ad1c3..b127e426 100644 --- a/core/src/intents/token_diff.rs +++ b/core/src/intents/token_diff.rs @@ -86,7 +86,7 @@ impl ExecutableIntent for TokenDiff { engine .inspector - .on_token_diff(signer_id, &self, &fees_collected, intent_hash); + .on_token_diff(signer_id, &self, &fees_collected, intent_hash)?; // deposit fees to collector if !fees_collected.is_empty() { diff --git a/core/src/intents/tokens.rs b/core/src/intents/tokens.rs index 7f127ab6..d25a8bd6 100644 --- a/core/src/intents/tokens.rs +++ b/core/src/intents/tokens.rs @@ -47,7 +47,9 @@ impl ExecutableIntent for Transfer { if sender_id == self.receiver_id || self.tokens.is_empty() { return Err(DefuseError::InvalidIntent); } - engine.inspector.on_transfer(sender_id, &self, intent_hash); + engine + .inspector + .on_transfer(sender_id, &self, intent_hash)?; engine .state .internal_sub_balance(sender_id, self.tokens.clone())?; @@ -59,7 +61,7 @@ impl ExecutableIntent for Transfer { } #[near(serializers = [borsh, json])] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] /// Withdraw given FT tokens from the intents contract to a given external account id (external being outside of intents). pub struct FtWithdraw { pub token: AccountId, @@ -95,7 +97,7 @@ impl ExecutableIntent for FtWithdraw { { engine .inspector - .on_ft_withdraw(owner_id, &self, intent_hash); + .on_ft_withdraw(owner_id, &self, intent_hash)?; engine.state.ft_withdraw(owner_id, self) } } @@ -137,7 +139,7 @@ impl ExecutableIntent for NftWithdraw { { engine .inspector - .on_nft_withdraw(owner_id, &self, intent_hash); + .on_nft_withdraw(owner_id, &self, intent_hash)?; engine.state.nft_withdraw(owner_id, self) } } @@ -182,7 +184,7 @@ impl ExecutableIntent for MtWithdraw { { engine .inspector - .on_mt_withdraw(owner_id, &self, intent_hash); + .on_mt_withdraw(owner_id, &self, intent_hash)?; engine.state.mt_withdraw(owner_id, self) } } @@ -212,7 +214,7 @@ impl ExecutableIntent for NativeWithdraw { { engine .inspector - .on_native_withdraw(owner_id, &self, intent_hash); + .on_native_withdraw(owner_id, &self, intent_hash)?; engine.state.native_withdraw(owner_id, self) } } @@ -250,7 +252,7 @@ impl ExecutableIntent for StorageDeposit { { engine .inspector - .on_storage_deposit(owner_id, &self, intent_hash); + .on_storage_deposit(owner_id, &self, intent_hash)?; engine.state.storage_deposit(owner_id, self) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 7222dadd..0de8b0ea 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,7 +1,7 @@ pub mod accounts; mod deadline; pub mod engine; -mod error; +pub mod error; pub mod events; pub mod fees; pub mod intents; diff --git a/core/src/tokens.rs b/core/src/tokens.rs index 1cf7ec99..6fc09c55 100644 --- a/core/src/tokens.rs +++ b/core/src/tokens.rs @@ -112,9 +112,16 @@ pub enum ParseTokenIdError { #[near(serializers = [borsh, json])] #[autoimpl(Deref using self.0)] +#[autoimpl(DerefMut using self.0)] #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Amounts>(T); +impl From for Amounts { + fn from(m: T) -> Self { + Amounts(m) + } +} + impl Amounts { #[inline] pub const fn new(map: T) -> Self { diff --git a/defuse/src/contract/intents/execute.rs b/defuse/src/contract/intents/execute.rs index 922fc402..81e29cba 100644 --- a/defuse/src/contract/intents/execute.rs +++ b/defuse/src/contract/intents/execute.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use defuse_core::error::Result; use defuse_core::{ Deadline, accounts::AccountEvent, @@ -29,7 +30,7 @@ impl Inspector for ExecuteInspector { sender_id: &AccountIdRef, transfer: &Transfer, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::Transfer( [IntentEvent::new( AccountEvent::new(sender_id, Cow::Borrowed(transfer)), @@ -39,6 +40,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } #[inline] @@ -48,7 +51,7 @@ impl Inspector for ExecuteInspector { token_diff: &TokenDiff, fees_collected: &Amounts, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::TokenDiff( [IntentEvent::new( AccountEvent::new( @@ -64,6 +67,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } fn on_ft_withdraw( @@ -71,7 +76,7 @@ impl Inspector for ExecuteInspector { owner_id: &AccountIdRef, ft_withdraw: &FtWithdraw, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::FtWithdraw( [IntentEvent::new( AccountEvent::new(owner_id, Cow::Borrowed(ft_withdraw)), @@ -81,6 +86,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } fn on_nft_withdraw( @@ -88,7 +95,7 @@ impl Inspector for ExecuteInspector { owner_id: &AccountIdRef, nft_withdraw: &NftWithdraw, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::NftWithdraw( [IntentEvent::new( AccountEvent::new(owner_id, Cow::Borrowed(nft_withdraw)), @@ -98,6 +105,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } fn on_mt_withdraw( @@ -105,7 +114,7 @@ impl Inspector for ExecuteInspector { owner_id: &AccountIdRef, mt_withdraw: &MtWithdraw, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::MtWithdraw( [IntentEvent::new( AccountEvent::new(owner_id, Cow::Borrowed(mt_withdraw)), @@ -115,6 +124,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } fn on_native_withdraw( @@ -122,7 +133,7 @@ impl Inspector for ExecuteInspector { owner_id: &AccountIdRef, native_withdraw: &NativeWithdraw, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::NativeWithdraw( [IntentEvent::new( AccountEvent::new(owner_id, Cow::Borrowed(native_withdraw)), @@ -132,6 +143,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } fn on_storage_deposit( @@ -139,7 +152,7 @@ impl Inspector for ExecuteInspector { owner_id: &AccountIdRef, storage_deposit: &StorageDeposit, intent_hash: CryptoHash, - ) { + ) -> Result<()> { DefuseEvent::StorageDeposit( [IntentEvent::new( AccountEvent::new(owner_id, Cow::Borrowed(storage_deposit)), @@ -149,6 +162,8 @@ impl Inspector for ExecuteInspector { .into(), ) .emit(); + + Ok(()) } #[inline] diff --git a/defuse/src/contract/intents/mod.rs b/defuse/src/contract/intents/mod.rs index 7c965f58..8c8f5eb7 100644 --- a/defuse/src/contract/intents/mod.rs +++ b/defuse/src/contract/intents/mod.rs @@ -35,7 +35,7 @@ impl Intents for Contract { #[pause(name = "intents")] #[inline] fn simulate_intents(&self, signed: Vec) -> SimulationOutput { - let mut inspector = SimulateInspector::default(); + let mut inspector = SimulateInspector::new(self.wnear_id.clone()); let engine = Engine::new(self.cached(), &mut inspector); let invariant_violated = match engine.execute_signed_intents(signed) { @@ -50,6 +50,10 @@ impl Intents for Contract { min_deadline: inspector.min_deadline, invariant_violated, state: StateOutput { fee: self.fee() }, + balance_diff: inspector.balance_diff, + ft_withdrawals: inspector.ft_withdrawals, + nft_withdrawals: inspector.nft_withdrawals, + mt_withdrawals: inspector.mt_withdrawals, } } } diff --git a/defuse/src/contract/intents/simulate.rs b/defuse/src/contract/intents/simulate.rs index a8a2bda0..8e58ca2d 100644 --- a/defuse/src/contract/intents/simulate.rs +++ b/defuse/src/contract/intents/simulate.rs @@ -1,26 +1,42 @@ +use std::collections::HashMap; + +use defuse_core::DefuseError; +use defuse_core::error::Result; use defuse_core::{ Deadline, accounts::AccountEvent, engine::Inspector, intents::{ IntentEvent, - token_diff::TokenDiff, + token_diff::{TokenDeltas, TokenDiff}, tokens::{FtWithdraw, MtWithdraw, NativeWithdraw, NftWithdraw, StorageDeposit, Transfer}, }, - tokens::Amounts, + tokens::{Amounts, TokenId}, }; -use near_sdk::{AccountIdRef, CryptoHash}; +use defuse_map_utils::cleanup::DefaultMap; +use defuse_near_utils::UnwrapOrPanicError; +use near_sdk::{AccountId, AccountIdRef, CryptoHash, require}; pub struct SimulateInspector { pub intents_executed: Vec>>, pub min_deadline: Deadline, + pub balance_diff: HashMap, + pub wnear_id: AccountId, + pub ft_withdrawals: Option>, + pub nft_withdrawals: Option>, + pub mt_withdrawals: Option>, } -impl Default for SimulateInspector { - fn default() -> Self { +impl SimulateInspector { + pub fn new(wnear_id: AccountId) -> Self { Self { intents_executed: Vec::new(), min_deadline: Deadline::MAX, + balance_diff: HashMap::default(), + wnear_id, + ft_withdrawals: None, + nft_withdrawals: None, + mt_withdrawals: None, } } } @@ -34,60 +50,155 @@ impl Inspector for SimulateInspector { #[inline] fn on_transfer( &mut self, - _sender_id: &AccountIdRef, - _transfer: &Transfer, + sender_id: &AccountIdRef, + transfer: &Transfer, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + for (token_id, transfer_amount) in &transfer.tokens { + self.balance_diff + .entry_or_default(sender_id.to_owned()) + .sub(token_id.clone(), *transfer_amount) + .ok_or(DefuseError::BalanceOverflow)?; + + self.balance_diff + .entry_or_default(transfer.receiver_id.clone()) + .add(token_id.clone(), *transfer_amount) + .ok_or(DefuseError::BalanceOverflow)?; + } + + Ok(()) } #[inline] fn on_token_diff( &mut self, - _owner_id: &AccountIdRef, - _token_diff: &TokenDiff, + owner_id: &AccountIdRef, + token_diff: &TokenDiff, _fees_collected: &Amounts, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + for (token_id, delta) in &token_diff.diff { + if *delta >= 0 { + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .add( + token_id.clone(), + (*delta).try_into().unwrap_or_panic_display(), + ) + .ok_or(DefuseError::BalanceOverflow)?; + } else { + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .sub( + token_id.clone(), + (-delta).try_into().unwrap_or_panic_display(), + ) + .ok_or(DefuseError::BalanceOverflow)?; + } + } + + Ok(()) } fn on_ft_withdraw( &mut self, - _owner_id: &AccountIdRef, - _ft_withdraw: &FtWithdraw, + owner_id: &AccountIdRef, + ft_withdraw: &FtWithdraw, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .sub( + TokenId::Nep141(ft_withdraw.token.clone()), + ft_withdraw.amount.0, + ) + .ok_or(DefuseError::BalanceOverflow)?; + + self.ft_withdrawals + .get_or_insert(Vec::new()) + .push(ft_withdraw.clone()); + + Ok(()) } fn on_nft_withdraw( &mut self, - _owner_id: &AccountIdRef, - _nft_withdraw: &NftWithdraw, + owner_id: &AccountIdRef, + nft_withdraw: &NftWithdraw, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .sub( + TokenId::Nep171(nft_withdraw.token.clone(), nft_withdraw.token_id.clone()), + 1, + ) + .ok_or(DefuseError::BalanceOverflow)?; + + self.nft_withdrawals + .get_or_insert(Vec::new()) + .push(nft_withdraw.clone()); + + Ok(()) } fn on_mt_withdraw( &mut self, - _owner_id: &AccountIdRef, - _mt_withdraw: &MtWithdraw, + owner_id: &AccountIdRef, + mt_withdraw: &MtWithdraw, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + require!( + mt_withdraw.amounts.len() != mt_withdraw.token_ids.len(), + "Invalid mt_withdraw() call. List of tokens and amounts don't match in length." + ); + + for (token_id, transfer_amount) in + mt_withdraw.token_ids.iter().zip(mt_withdraw.amounts.iter()) + { + let token_id: TokenId = token_id.parse().unwrap_or_panic_display(); + + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .sub(token_id, transfer_amount.0) + .ok_or(DefuseError::BalanceOverflow)?; + } + + self.mt_withdrawals + .get_or_insert(Vec::new()) + .push(mt_withdraw.clone()); + + Ok(()) } fn on_native_withdraw( &mut self, - _owner_id: &AccountIdRef, - _native_withdraw: &NativeWithdraw, + owner_id: &AccountIdRef, + native_withdraw: &NativeWithdraw, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + let wnear = TokenId::Nep141(self.wnear_id.clone()); + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .sub(wnear, native_withdraw.amount.as_yoctonear()) + .ok_or(DefuseError::BalanceOverflow)?; + + Ok(()) } fn on_storage_deposit( &mut self, - _owner_id: &AccountIdRef, - _storage_deposit: &StorageDeposit, + owner_id: &AccountIdRef, + storage_deposit: &StorageDeposit, _intent_hash: CryptoHash, - ) { + ) -> Result<()> { + let wnear = TokenId::Nep141(self.wnear_id.clone()); + self.balance_diff + .entry_or_default(owner_id.to_owned()) + .sub(wnear, storage_deposit.amount.as_yoctonear()) + .ok_or(DefuseError::BalanceOverflow)?; + + Ok(()) } #[inline] diff --git a/defuse/src/intents.rs b/defuse/src/intents.rs index 35e117b8..9e7f84dc 100644 --- a/defuse/src/intents.rs +++ b/defuse/src/intents.rs @@ -1,10 +1,20 @@ +use std::collections::HashMap; + use defuse_core::{ - Deadline, Result, accounts::AccountEvent, engine::deltas::InvariantViolated, fees::Pips, - intents::IntentEvent, payload::multi::MultiPayload, + Deadline, Result, + accounts::AccountEvent, + engine::deltas::InvariantViolated, + fees::Pips, + intents::{ + IntentEvent, + token_diff::TokenDeltas, + tokens::{FtWithdraw, MtWithdraw, NftWithdraw}, + }, + payload::multi::MultiPayload, }; use near_plugins::AccessControllable; -use near_sdk::{Promise, PublicKey, ext_contract, near}; +use near_sdk::{AccountId, Promise, PublicKey, ext_contract, near}; use serde_with::serde_as; use crate::fees::FeesManager; @@ -40,6 +50,14 @@ pub struct SimulationOutput { /// Additional info about current state pub state: StateOutput, + + /// All changes in balances after simulating the intent + pub balance_diff: HashMap, + + /// Explicit withdrawal requests + pub ft_withdrawals: Option>, + pub nft_withdrawals: Option>, + pub mt_withdrawals: Option>, } impl SimulationOutput { diff --git a/tests/src/tests/defuse/intents/ft_withdraw.rs b/tests/src/tests/defuse/intents/ft_withdraw.rs index e0f46415..3425d57d 100644 --- a/tests/src/tests/defuse/intents/ft_withdraw.rs +++ b/tests/src/tests/defuse/intents/ft_withdraw.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{collections::BTreeMap, time::Duration}; use defuse::{ contract::config::{DefuseConfig, RolesConfig}, @@ -6,7 +6,7 @@ use defuse::{ Deadline, fees::{FeesConfig, Pips}, intents::{DefuseIntents, tokens::FtWithdraw}, - tokens::TokenId, + tokens::{Amounts, TokenId}, }, }; use near_sdk::{AccountId, NearToken}; @@ -250,26 +250,39 @@ async fn test_ft_withdraw_intent_msg( .await .unwrap(); - env.defuse - .execute_intents([env.user1.sign_defuse_message( - env.defuse.id(), - rng.random(), - Deadline::timeout(Duration::from_secs(120)), - DefuseIntents { - intents: [FtWithdraw { - token: env.ft1.clone(), - receiver_id: defuse2.id().clone(), - amount: 1000.into(), - memo: Some("defuse-to-defuse".to_string()), - msg: Some(env.user2.id().to_string()), - storage_deposit: None, - } - .into()] - .into(), - }, - )]) - .await - .unwrap(); + let withdraw_intent = FtWithdraw { + token: env.ft1.clone(), + receiver_id: defuse2.id().clone(), + amount: 1000.into(), + memo: Some("defuse-to-defuse".to_string()), + msg: Some(env.user2.id().to_string()), + storage_deposit: None, + }; + + let intents = [env.user1.sign_defuse_message( + env.defuse.id(), + rng.random(), + Deadline::timeout(Duration::from_secs(120)), + DefuseIntents { + intents: [withdraw_intent.clone().into()].into(), + }, + )]; + + let sim_out = env.defuse.simulate_intents(intents.clone()).await.unwrap(); + assert_eq!(sim_out.balance_diff.len(), 1); + assert_eq!( + sim_out.balance_diff.get(env.user1.id()).unwrap(), + &Amounts::from( + [(TokenId::Nep141(env.ft1.clone()), -1000)] + .into_iter() + .collect::>() + ) + ); + assert_eq!(sim_out.ft_withdrawals, Some(vec![withdraw_intent])); + assert!(sim_out.nft_withdrawals.is_none()); + assert!(sim_out.mt_withdrawals.is_none()); + + env.defuse.execute_intents(intents).await.unwrap(); let ft1 = TokenId::Nep141(env.ft1.clone()); diff --git a/tests/src/tests/defuse/intents/mod.rs b/tests/src/tests/defuse/intents/mod.rs index 480061f2..a1a491cc 100644 --- a/tests/src/tests/defuse/intents/mod.rs +++ b/tests/src/tests/defuse/intents/mod.rs @@ -92,11 +92,12 @@ impl ExecuteIntentsExt for near_workspaces::Account { "simulate_intents({})", serde_json::to_string_pretty(&args).unwrap() ); - self.view(defuse_id, "simulate_intents") + let res = self + .view(defuse_id, "simulate_intents") .args_json(args) - .await? - .json() - .map_err(Into::into) + .await?; + println!("Simulation logs: {:?}", res.logs); + res.json().map_err(Into::into) } async fn simulate_intents( &self, diff --git a/tests/src/tests/defuse/intents/token_diff.rs b/tests/src/tests/defuse/intents/token_diff.rs index 9491dfd2..ecb740a6 100644 --- a/tests/src/tests/defuse/intents/token_diff.rs +++ b/tests/src/tests/defuse/intents/token_diff.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeMap, time::Duration}; +use std::{ + collections::{BTreeMap, HashMap}, + time::Duration, +}; use defuse::core::{ Deadline, @@ -8,7 +11,7 @@ use defuse::core::{ token_diff::{TokenDeltas, TokenDiff}, }, payload::multi::MultiPayload, - tokens::TokenId, + tokens::{Amounts, TokenId}, }; use near_sdk::AccountId; use near_workspaces::Account; @@ -179,7 +182,7 @@ async fn test_swap_many( type FtBalances<'a> = BTreeMap<&'a AccountId, i128>; -#[derive(Debug)] +#[derive(Debug, Clone)] struct AccountFtDiff<'a> { account: &'a Account, init_balances: FtBalances<'a>, @@ -187,6 +190,34 @@ struct AccountFtDiff<'a> { result_balances: FtBalances<'a>, } +fn into_simulation_diff( + test_diffs: Vec, +) -> HashMap>> { + let mut result = HashMap::>>::default(); + + for account_diffs in test_diffs { + for token_diffs in account_diffs.diff { + for (token_id, amount_diff) in token_diffs { + if amount_diff >= 0 { + result + .entry(account_diffs.account.id().to_owned()) + .or_default() + .add(token_id, amount_diff.try_into().unwrap()) + .unwrap(); + } else { + result + .entry(account_diffs.account.id().to_owned()) + .or_default() + .sub(token_id, (-amount_diff).try_into().unwrap()) + .unwrap(); + } + } + } + } + + result +} + async fn test_ft_diffs(env: &Env, accounts: Vec>) { // deposit for account in &accounts { @@ -224,12 +255,12 @@ async fn test_ft_diffs(env: &Env, accounts: Vec>) { .collect(); // simulate - env.defuse - .simulate_intents(signed.clone()) - .await - .unwrap() - .into_result() - .unwrap(); + let sim_res = env.defuse.simulate_intents(signed.clone()).await.unwrap(); + assert_eq!(sim_res.balance_diff, into_simulation_diff(accounts.clone())); + println!("Balance diff: {:#?}", sim_res.balance_diff); + + // Unwrap simulation result (no invariants violated) + sim_res.into_result().unwrap(); // verify env.defuse.execute_intents(signed).await.unwrap();