diff --git a/near/omni-bridge/src/lib.rs b/near/omni-bridge/src/lib.rs index 0f8054fe..27841cd6 100644 --- a/near/omni-bridge/src/lib.rs +++ b/near/omni-bridge/src/lib.rs @@ -24,7 +24,8 @@ use omni_types::near_events::OmniBridgeEvent; use omni_types::prover_args::VerifyProofArgs; use omni_types::prover_result::ProverResult; use omni_types::{ - BasicMetadata, ChainKind, Fee, InitTransferMsg, MetadataPayload, Nonce, OmniAddress, + BasicMetadata, BridgeOnTransferMsg, ChainKind, FastFinTransferMsg, FastTransfer, + FastTransferId, FastTransferStatus, Fee, InitTransferMsg, MetadataPayload, Nonce, OmniAddress, PayloadType, SignRequest, TransferId, TransferMessage, TransferMessagePayload, UpdateFee, }; use storage::{Decimals, TransferMessageStorage, TransferMessageStorageValue, NEP141_DEPOSIT}; @@ -55,6 +56,8 @@ const DEPLOY_TOKEN_GAS: Gas = Gas::from_tgas(50); const BURN_TOKEN_GAS: Gas = Gas::from_tgas(10); const MINT_TOKEN_GAS: Gas = Gas::from_tgas(5); const SET_METADATA_GAS: Gas = Gas::from_tgas(10); +const RESOLVE_TRANSFER_GAS: Gas = Gas::from_tgas(3); +const FAST_TRANSFER_CALLBACK_GAS: Gas = Gas::from_tgas(5); const NO_DEPOSIT: NearToken = NearToken::from_near(0); const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1); const SIGN_PATH: &str = "bridge-1"; @@ -70,6 +73,7 @@ enum StorageKey { TokenDeployerAccounts, DeployedTokens, DestinationNonces, + FastTransfers, TokenDecimals, } @@ -164,6 +168,7 @@ pub struct Contract { pub factories: LookupMap, pub pending_transfers: LookupMap, pub finalised_transfers: LookupSet, + pub fast_transfers: LookupMap, pub token_id_to_address: LookupMap<(ChainKind, AccountId), OmniAddress>, pub token_address_to_id: LookupMap, pub token_decimals: LookupMap, @@ -186,53 +191,35 @@ impl FungibleTokenReceiver for Contract { amount: U128, msg: String, ) -> PromiseOrValue { - let parsed_msg: InitTransferMsg = serde_json::from_str(&msg).sdk_expect("ERR_PARSE_MSG"); let token_id = env::predecessor_account_id(); - require!( - parsed_msg.recipient.get_chain() != ChainKind::Near, - "ERR_INVALID_RECIPIENT_CHAIN" - ); - - self.current_origin_nonce += 1; - let destination_nonce = self.get_next_destination_nonce(parsed_msg.recipient.get_chain()); - - let transfer_message = TransferMessage { - origin_nonce: self.current_origin_nonce, - token: OmniAddress::Near(token_id.clone()), - amount, - recipient: parsed_msg.recipient, - fee: Fee { - fee: parsed_msg.fee, - native_fee: parsed_msg.native_token_fee, - }, - sender: OmniAddress::Near(sender_id.clone()), - msg: String::new(), - destination_nonce, + let parsed_msg: BridgeOnTransferMsg = + serde_json::from_str(&msg).sdk_expect("ERR_PARSE_MSG"); + let promise_or_value = match parsed_msg { + BridgeOnTransferMsg::InitTransfer(init_transfer_msg) => PromiseOrValue::Value( + self.init_transfer(sender_id, token_id.clone(), amount, init_transfer_msg), + ), + BridgeOnTransferMsg::FastFinTransfer(fast_fin_transfer_msg) => { + self.fast_fin_transfer(sender_id, token_id.clone(), amount, fast_fin_transfer_msg) + } }; - require!( - transfer_message.fee.fee < transfer_message.amount, - "ERR_INVALID_FEE" - ); - let mut required_storage_balance = - self.add_transfer_message(transfer_message.clone(), sender_id.clone()); - required_storage_balance = required_storage_balance - .saturating_add(NearToken::from_yoctonear(parsed_msg.native_token_fee.0)); - - self.update_storage_balance( - sender_id, - required_storage_balance, - NearToken::from_yoctonear(0), - ); - - if self.deployed_tokens.contains(&token_id) { - ext_token::ext(token_id.clone()) - .with_static_gas(BURN_TOKEN_GAS) - .burn(amount); + if !self.deployed_tokens.contains(&token_id) { + return promise_or_value; } - env::log_str(&OmniBridgeEvent::InitTransferEvent { transfer_message }.to_log_string()); - PromiseOrValue::Value(U128(0)) + match promise_or_value { + PromiseOrValue::Promise(promise) => PromiseOrValue::Promise( + promise.then( + Self::ext(env::current_account_id()) + .with_static_gas(BURN_TOKEN_GAS) + .burn_tokens(token_id, amount), + ), + ), + PromiseOrValue::Value(_) => { + self.burn_tokens(token_id, amount); + promise_or_value + } + } } } @@ -249,6 +236,7 @@ impl Contract { factories: LookupMap::new(StorageKey::Factories), pending_transfers: LookupMap::new(StorageKey::PendingTransfers), finalised_transfers: LookupSet::new(StorageKey::FinalisedTransfers), + fast_transfers: LookupMap::new(StorageKey::FastTransfers), token_id_to_address: LookupMap::new(StorageKey::TokenIdToAddress), token_address_to_id: LookupMap::new(StorageKey::TokenAddressToId), token_decimals: LookupMap::new(StorageKey::TokenDecimals), @@ -444,6 +432,58 @@ impl Contract { ) } + fn init_transfer( + &mut self, + sender_id: AccountId, + token_id: AccountId, + amount: U128, + init_transfer_msg: InitTransferMsg, + ) -> U128 { + require!( + init_transfer_msg.recipient.get_chain() != ChainKind::Near, + "ERR_INVALID_RECIPIENT_CHAIN" + ); + + self.current_origin_nonce += 1; + let destination_nonce = + self.get_next_destination_nonce(init_transfer_msg.recipient.get_chain()); + + let transfer_message = TransferMessage { + origin_nonce: self.current_origin_nonce, + token: OmniAddress::Near(token_id.clone()), + amount, + recipient: init_transfer_msg.recipient, + fee: Fee { + fee: init_transfer_msg.fee, + native_fee: init_transfer_msg.native_token_fee, + }, + sender: OmniAddress::Near(sender_id.clone()), + msg: String::new(), + destination_nonce, + origin_transfer_id: None, + }; + require!( + transfer_message.fee.fee < transfer_message.amount, + "ERR_INVALID_FEE" + ); + + let mut required_storage_balance = + self.add_transfer_message(transfer_message.clone(), sender_id.clone()); + required_storage_balance = required_storage_balance.saturating_add( + NearToken::from_yoctonear(init_transfer_msg.native_token_fee.0), + ); + + self.update_storage_balance( + sender_id, + required_storage_balance, + NearToken::from_yoctonear(0), + ); + + env::log_str(&OmniBridgeEvent::InitTransferEvent { transfer_message }.to_log_string()); + + U128(0) + } + #[private] pub fn sign_transfer_callback( &mut self, @@ -473,7 +513,7 @@ impl Contract { args.storage_deposit_actions.len() <= 3, "Invalid len of accounts for storage deposit" ); - let main_promise = ext_prover::ext(self.prover_account.clone()) + let mut main_promise = ext_prover::ext(self.prover_account.clone()) .with_static_gas(VERIFY_PROOF_GAS) .with_attached_deposit(NO_DEPOSIT) .verify_proof(VerifyProofArgs { @@ -482,12 +522,13 @@ impl Contract { }); let mut attached_deposit = env::attached_deposit(); - Self::check_or_pay_ft_storage( - main_promise, - &args.storage_deposit_actions, - &mut attached_deposit, - ) - .then( + + for action in &args.storage_deposit_actions { + main_promise = + main_promise.and(Self::check_or_pay_ft_storage(action, &mut attached_deposit)); + } + + main_promise.then( Self::ext(env::current_account_id()) .with_attached_deposit(attached_deposit) .with_static_gas(VERIFY_PROOF_CALLBACK_GAS) @@ -534,6 +575,7 @@ impl Contract { sender: init_transfer.sender, msg: init_transfer.msg, destination_nonce, + origin_transfer_id: None, }; if let OmniAddress::Near(recipient) = transfer_message.recipient.clone() { @@ -545,11 +587,150 @@ impl Contract { ) .into() } else { - self.process_fin_transfer_to_other_cahin(predecessor_account_id, transfer_message); + self.process_fin_transfer_to_other_chain(predecessor_account_id, transfer_message); PromiseOrValue::Value(destination_nonce) } } + fn fast_fin_transfer( + &mut self, + sender_id: AccountId, + token_id: AccountId, + amount: U128, + fast_fin_transfer_msg: FastFinTransferMsg, + ) -> PromiseOrValue { + let fast_transfer = FastTransfer { + token_id: token_id.clone(), + recipient: fast_fin_transfer_msg.recipient.clone(), + amount: U128(amount.0 + fast_fin_transfer_msg.fee.fee.0), + fee: fast_fin_transfer_msg.fee, + transfer_id: fast_fin_transfer_msg.transfer_id, + msg: fast_fin_transfer_msg.msg, + }; + + match fast_fin_transfer_msg.recipient { + OmniAddress::Near(recipient) => { + let storage_deposit_amount = fast_fin_transfer_msg + .storage_deposit_amount + .unwrap_or_default(); + if storage_deposit_amount > 0 { + self.update_storage_balance( + sender_id.clone(), + NearToken::from_yoctonear(storage_deposit_amount), + NearToken::from_yoctonear(0), + ); + } + + let deposit_action = StorageDepositAction { + account_id: recipient, + token_id, + storage_deposit_amount: fast_fin_transfer_msg.storage_deposit_amount, + }; + PromiseOrValue::Promise( + Self::check_or_pay_ft_storage( + &deposit_action, + &mut NearToken::from_yoctonear(storage_deposit_amount), + ) + .then( + Self::ext(env::current_account_id()) + .with_static_gas( + FAST_TRANSFER_CALLBACK_GAS.saturating_add(FT_TRANSFER_CALL_GAS), + ) + .fast_fin_transfer_to_near_callback(fast_transfer, sender_id), + ), + ) + } + _ => { + self.fast_fin_transfer_to_other_chain(fast_transfer, sender_id); + PromiseOrValue::Value(U128(0)) + } + } + } + + #[private] + pub fn fast_fin_transfer_to_near_callback( + &mut self, + #[serializer(borsh)] fast_transfer: FastTransfer, + #[serializer(borsh)] relayer_id: AccountId, + ) -> Promise { + require!( + Self::check_storage_balance_result(0), + "STORAGE_ERR: The transfer recipient is omitted" + ); + + let OmniAddress::Near(recipient) = fast_transfer.recipient.clone() else { + env::panic_str("ERR_INVALID_STATE") + }; + + let required_balance = self + .add_fast_transfer(&fast_transfer, relayer_id.clone()) + .saturating_add(ONE_YOCTO); + self.update_storage_balance(relayer_id, required_balance, NearToken::from_yoctonear(0)); + + env::log_str( + &OmniBridgeEvent::FastTransferEvent { + fast_transfer: fast_transfer.clone(), + new_transfer_id: None, + } + .to_log_string(), + ); + + self.send_tokens( + fast_transfer.token_id, + recipient, + U128(fast_transfer.amount.0 - fast_transfer.fee.fee.0), + fast_transfer.msg, + ) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(RESOLVE_TRANSFER_GAS) + .resolve_transfer(fast_transfer.amount), + ) + } + + fn fast_fin_transfer_to_other_chain( + &mut self, + fast_transfer: FastTransfer, + relayer_id: AccountId, + ) { + if self.is_transfer_finalised(fast_transfer.transfer_id) { + env::panic_str("ERR_TRANSFER_ALREADY_FINALISED"); + } + + let mut required_balance = self.add_fast_transfer(&fast_transfer, relayer_id.clone()); + + let destination_nonce = + self.get_next_destination_nonce(fast_transfer.recipient.get_chain()); + self.current_origin_nonce += 1; + + let transfer_message = TransferMessage { + origin_nonce: self.current_origin_nonce, + token: OmniAddress::Near(fast_transfer.token_id.clone()), + amount: fast_transfer.amount, + recipient: fast_transfer.recipient.clone(), + fee: fast_transfer.fee.clone(), + sender: OmniAddress::Near(relayer_id.clone()), + msg: fast_transfer.msg.clone(), + destination_nonce, + origin_transfer_id: Some(fast_transfer.transfer_id), + }; + let new_transfer_id = transfer_message.get_transfer_id(); + + required_balance = self + .add_transfer_message(transfer_message, relayer_id.clone()) + .saturating_add(required_balance); + + env::log_str( + &OmniBridgeEvent::FastTransferEvent { + fast_transfer: fast_transfer.clone(), + new_transfer_id: Some(new_transfer_id), + } + .to_log_string(), + ); + + self.update_storage_balance(relayer_id, required_balance, NearToken::from_near(0)); + } + #[payable] #[pause(except(roles(Role::DAO, Role::UnrestrictedRelayer)))] pub fn claim_fee(&mut self, #[serializer(borsh)] args: ClaimFeeArgs) -> Promise { @@ -593,23 +774,24 @@ impl Contract { ); let message = self.remove_transfer_message(fin_transfer.transfer_id); - let token_address = self - .get_token_address( - message.get_destination_chain(), - self.get_token_id(&message.token), - ) - .unwrap_or_else(|| env::panic_str("ERR_FAILED_TO_GET_TOKEN_ADDRESS")); - let denormalized_amount = Self::denormalize_amount( - fin_transfer.amount.0, - self.token_decimals - .get(&token_address) - .sdk_expect("ERR_TOKEN_DECIMALS_NOT_FOUND"), - ); - let fee = message.amount.0 - denormalized_amount; + // Need to make sure fast transfer is finalised because it means transfer parameters are correct. Otherwise, fee can be set as anything. + if let Some(origin_transfer_id) = message.origin_transfer_id { + let mut fast_transfer = + FastTransfer::from_transfer(message.clone(), self.get_token_id(&message.token)); + fast_transfer.transfer_id = origin_transfer_id; + require!( + self.is_fast_transfer_finalised(fast_transfer.id()), + "ERR_FAST_TRANSFER_NOT_FINALISED" + ); + } if message.fee.native_fee.0 != 0 { - if message.get_origin_chain() == ChainKind::Near { + let origin_chain = match message.origin_transfer_id { + Some(origin_transfer_id) => origin_transfer_id.origin_chain, + None => message.get_origin_chain(), + }; + if origin_chain == ChainKind::Near { Promise::new(fin_transfer.fee_recipient.clone()) .transfer(NearToken::from_yoctonear(message.fee.native_fee.0)); } else { @@ -626,11 +808,23 @@ impl Contract { let token = self.get_token_id(&message.token); env::log_str( &OmniBridgeEvent::ClaimFeeEvent { - transfer_message: message, + transfer_message: message.clone(), } .to_log_string(), ); + let token_address = self + .get_token_address(message.get_destination_chain(), token.clone()) + .unwrap_or_else(|| env::panic_str("ERR_FAILED_TO_GET_TOKEN_ADDRESS")); + + let denormalized_amount = Self::denormalize_amount( + fin_transfer.amount.0, + self.token_decimals + .get(&token_address) + .sdk_expect("ERR_TOKEN_DECIMALS_NOT_FOUND"), + ); + let fee = message.amount.0 - denormalized_amount; + if fee > 0 { if self.deployed_tokens.contains(&token) { PromiseOrValue::Promise(ext_token::ext(token).with_static_gas(MINT_TOKEN_GAS).mint( @@ -851,6 +1045,13 @@ impl Contract { self.finalised_transfers.contains(&transfer_id) } + pub fn is_fast_transfer_finalised(&self, fast_transfer_id: FastTransferId) -> bool { + self.fast_transfers + .get(&fast_transfer_id) + .map(|status| status.finalised) + .unwrap_or(false) + } + #[access_control_any(roles(Role::DAO))] pub fn add_factory(&mut self, address: OmniAddress) { self.factories.insert(&(&address).into(), &address); @@ -911,6 +1112,27 @@ impl Contract { self.destination_nonces.get(&chain_kind).unwrap_or_default() } + #[private] + pub fn resolve_transfer(&mut self, _amount: U128) -> U128 { + U128(0) + } + + #[private] + pub fn burn_tokens(&self, token: AccountId, amount: U128) -> Promise { + if env::promise_results_count() == 0 { + return ext_token::ext(token) + .with_static_gas(BURN_TOKEN_GAS) + .burn(amount); + } + + match env::promise_result(0) { + PromiseResult::Failed => env::panic_str("ERR_FAST_TRANSFER_FAILED"), + PromiseResult::Successful(_) => ext_token::ext(token) + .with_static_gas(BURN_TOKEN_GAS) + .burn(amount), + } + } + pub fn get_mpc_account(&self) -> AccountId { self.mpc_signer.clone() } @@ -943,6 +1165,23 @@ impl Contract { let token = self.get_token_id(&transfer_message.token); + // If fast transfer happened, change recipient to the relayer that executed fast transfer + let fast_transfer = FastTransfer::from_transfer(transfer_message.clone(), token.clone()); + let (recipient, is_fast_transfer) = match self.fast_transfers.get(&fast_transfer.id()) { + Some(status) => { + require!( + predecessor_account_id == *status.relayer, + "ERR_FAST_TRANSFER_PERFORMED_BY_ANOTHER_RELAYER" + ); + (status.relayer, true) + } + None => (recipient, false), + }; + + if is_fast_transfer { + self.complete_fast_transfer(&fast_transfer.id()); + } + let mut storage_deposit_action_index: usize = 0; require!( Self::check_storage_balance_result( @@ -958,46 +1197,16 @@ impl Contract { let amount_to_transfer = U128(transfer_message.amount.0 - transfer_message.fee.fee.0); let is_deployed_token = self.deployed_tokens.contains(&token); - let mut promise = if token == self.wnear_account_id && transfer_message.msg.is_empty() { - // Unwrap wNEAR and transfer NEAR tokens - ext_wnear_token::ext(self.wnear_account_id.clone()) - .with_static_gas(WNEAR_WITHDRAW_GAS) - .with_attached_deposit(ONE_YOCTO) - .near_withdraw(amount_to_transfer) - .then( - Promise::new(recipient) - .transfer(NearToken::from_yoctonear(amount_to_transfer.0)), - ) - } else if is_deployed_token { - let deposit = if transfer_message.msg.is_empty() { - NO_DEPOSIT + let mut promise = self.send_tokens( + token.clone(), + recipient, + amount_to_transfer, + if is_fast_transfer { + String::new() } else { - ONE_YOCTO - }; - ext_token::ext(token.clone()) - .with_attached_deposit(deposit) - .with_static_gas(MINT_TOKEN_GAS.saturating_add(FT_TRANSFER_CALL_GAS)) - .mint( - recipient, - amount_to_transfer, - (!transfer_message.msg.is_empty()).then(|| transfer_message.msg.clone()), - ) - } else if transfer_message.msg.is_empty() { - ext_token::ext(token.clone()) - .with_attached_deposit(ONE_YOCTO) - .with_static_gas(FT_TRANSFER_GAS) - .ft_transfer(recipient, amount_to_transfer, None) - } else { - ext_token::ext(token.clone()) - .with_attached_deposit(ONE_YOCTO) - .with_static_gas(FT_TRANSFER_CALL_GAS) - .ft_transfer_call( - recipient, - amount_to_transfer, - None, - transfer_message.msg.clone(), - ) - }; + transfer_message.msg.clone() + }, + ); if transfer_message.fee.fee.0 > 0 { require!( @@ -1071,16 +1280,40 @@ impl Contract { promise } - fn process_fin_transfer_to_other_cahin( + fn process_fin_transfer_to_other_chain( &mut self, predecessor_account_id: AccountId, transfer_message: TransferMessage, ) { let mut required_balance = self.add_fin_transfer(&transfer_message.get_transfer_id()); + let token = self.get_token_id(&transfer_message.token); - required_balance = self - .add_transfer_message(transfer_message.clone(), predecessor_account_id.clone()) - .saturating_add(required_balance); + let fast_transfer = FastTransfer::from_transfer(transfer_message.clone(), token.clone()); + let recipient = match self.fast_transfers.get(&fast_transfer.id()) { + Some(status) => { + require!( + predecessor_account_id == *status.relayer, + "ERR_FAST_TRANSFER_PERFORMED_BY_ANOTHER_RELAYER" + ); + Some(status.relayer) + } + None => None, + }; + + // If fast transfer happened, send tokens to the relayer that executed fast transfer + if let Some(relayer) = recipient { + self.send_tokens( + token, + relayer.clone(), + U128(transfer_message.amount.0 - transfer_message.fee.fee.0), + String::new(), + ); + self.complete_fast_transfer(&fast_transfer.id()); + } else { + required_balance = self + .add_transfer_message(transfer_message.clone(), predecessor_account_id.clone()) + .saturating_add(required_balance); + } self.update_storage_balance( predecessor_account_id, @@ -1091,34 +1324,66 @@ impl Contract { env::log_str(&OmniBridgeEvent::FinTransferEvent { transfer_message }.to_log_string()); } - fn check_or_pay_ft_storage( - mut main_promise: Promise, - storage_deposit_actions: &Vec, - attached_deposit: &mut NearToken, + fn send_tokens( + &self, + token: AccountId, + recipient: AccountId, + amount: U128, + msg: String, ) -> Promise { - for action in storage_deposit_actions { - let promise = if let Some(storage_deposit_amount) = action.storage_deposit_amount { - let storage_deposit_amount = NearToken::from_yoctonear(storage_deposit_amount); - - *attached_deposit = attached_deposit - .checked_sub(storage_deposit_amount) - .sdk_expect("The attached deposit is less than required"); + let is_deployed_token = self.deployed_tokens.contains(&token); - ext_token::ext(action.token_id.clone()) - .with_static_gas(STORAGE_DEPOSIT_GAS) - .with_attached_deposit(storage_deposit_amount) - .storage_deposit(&action.account_id, Some(true)) + if token == self.wnear_account_id && msg.is_empty() { + // Unwrap wNEAR and transfer NEAR tokens + ext_wnear_token::ext(self.wnear_account_id.clone()) + .with_static_gas(WNEAR_WITHDRAW_GAS) + .with_attached_deposit(ONE_YOCTO) + .near_withdraw(amount) + .then(Promise::new(recipient).transfer(NearToken::from_yoctonear(amount.0))) + } else if is_deployed_token { + let deposit = if msg.is_empty() { + NO_DEPOSIT } else { - ext_token::ext(action.token_id.clone()) - .with_static_gas(STORAGE_BALANCE_OF_GAS) - .with_attached_deposit(NO_DEPOSIT) - .storage_balance_of(&action.account_id) + ONE_YOCTO }; - - main_promise = main_promise.and(promise); + ext_token::ext(token.clone()) + .with_attached_deposit(deposit) + .with_static_gas(MINT_TOKEN_GAS.saturating_add(FT_TRANSFER_CALL_GAS)) + .mint(recipient, amount, (!msg.is_empty()).then(|| msg.clone())) + } else if msg.is_empty() { + ext_token::ext(token.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(FT_TRANSFER_GAS) + .ft_transfer(recipient, amount, None) + } else { + ext_token::ext(token.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(FT_TRANSFER_CALL_GAS) + .ft_transfer_call(recipient, amount, None, msg.clone()) } + } + + fn check_or_pay_ft_storage( + action: &StorageDepositAction, + attached_deposit: &mut NearToken, + ) -> Promise { + if let Some(storage_deposit_amount) = action.storage_deposit_amount { + let storage_deposit_amount = NearToken::from_yoctonear(storage_deposit_amount); + + *attached_deposit = attached_deposit + .checked_sub(storage_deposit_amount) + .sdk_expect("The attached deposit is less than required"); - main_promise + ext_token::ext(action.token_id.clone()) + .with_static_gas(STORAGE_DEPOSIT_GAS) + .with_attached_deposit(storage_deposit_amount) + .storage_deposit(&action.account_id, Some(true)) + } else { + ext_token::ext(action.token_id.clone()) + .with_static_gas(STORAGE_BALANCE_OF_GAS) + .with_attached_deposit(NO_DEPOSIT) + .storage_balance_of(&action.account_id) + } } fn check_storage_balance_result(result_idx: u64) -> bool { @@ -1200,6 +1465,30 @@ impl Contract { .saturating_mul((env::storage_usage().saturating_sub(storage_usage)).into()) } + fn add_fast_transfer(&mut self, fast_transfer: &FastTransfer, relayer: AccountId) -> NearToken { + let storage_usage = env::storage_usage(); + require!( + self.fast_transfers + .insert( + &fast_transfer.id(), + &FastTransferStatus { + relayer, + finalised: false, + }, + ) + .is_none(), + "Fast transfer is already performed" + ); + env::storage_byte_cost() + .saturating_mul((env::storage_usage().saturating_sub(storage_usage)).into()) + } + + fn complete_fast_transfer(&mut self, fast_transfer_id: &FastTransferId) { + let mut fast_transfer = self.fast_transfers.get(fast_transfer_id).unwrap(); + fast_transfer.finalised = true; + self.fast_transfers.insert(fast_transfer_id, &fast_transfer); + } + fn update_storage_balance( &mut self, account_id: AccountId, diff --git a/near/omni-bridge/src/storage.rs b/near/omni-bridge/src/storage.rs index 921153df..59cd70b1 100644 --- a/near/omni-bridge/src/storage.rs +++ b/near/omni-bridge/src/storage.rs @@ -1,7 +1,7 @@ use near_contract_standards::storage_management::{StorageBalance, StorageBalanceBounds}; use near_sdk::{assert_one_yocto, borsh, near}; use near_sdk::{env, near_bindgen, AccountId, NearToken}; -use omni_types::TransferId; +use omni_types::{FastTransferStatus, TransferId}; use crate::{ require, ChainKind, Contract, ContractExt, Fee, OmniAddress, Promise, SdkExpect, @@ -166,6 +166,10 @@ impl Contract { sender: OmniAddress::Near(max_account_id.clone()), msg: String::new(), destination_nonce: 0, + origin_transfer_id: Some(TransferId { + origin_chain: ChainKind::Near, + origin_nonce: 0, + }), }, owner: max_account_id, })) @@ -179,7 +183,7 @@ impl Contract { } pub fn required_balance_for_fin_transfer(&self) -> NearToken { - let key_len: u64 = borsh::to_vec(&(ChainKind::Eth, 0_u128)) + let key_len: u64 = borsh::to_vec(&(ChainKind::Eth, 0_u64)) .sdk_expect("ERR_BORSH") .len() .try_into() @@ -192,6 +196,24 @@ impl Contract { storage_cost.saturating_add(ft_transfers_cost) } + pub fn required_balance_for_fast_transfer(&self) -> NearToken { + let key_len = borsh::to_vec(&[0u8; 32]).sdk_expect("ERR_BORSH").len() as u64; + + let max_account_id: AccountId = "a".repeat(64).parse().sdk_expect("ERR_PARSE_ACCOUNT_ID"); + let value_len = borsh::to_vec(&FastTransferStatus { + relayer: max_account_id, + finalised: false, + }) + .sdk_expect("ERR_BORSH") + .len() as u64; + + let storage_cost = env::storage_byte_cost() + .saturating_mul((Self::get_basic_storage() + key_len + value_len).into()); + let ft_transfers_cost = NearToken::from_yoctonear(1); + + storage_cost.saturating_add(ft_transfers_cost) + } + pub fn required_balance_for_bind_token(&self) -> NearToken { let max_token_id: AccountId = "a".repeat(64).parse().sdk_expect("ERR_PARSE_ACCOUNT_ID"); diff --git a/near/omni-bridge/src/tests/lib_test.rs b/near/omni-bridge/src/tests/lib_test.rs index d48de251..06847bc9 100644 --- a/near/omni-bridge/src/tests/lib_test.rs +++ b/near/omni-bridge/src/tests/lib_test.rs @@ -11,8 +11,8 @@ use near_sdk::{ use omni_types::{ locker_args::StorageDepositAction, prover_result::{InitTransferMessage, ProverResult}, - ChainKind, EvmAddress, Fee, InitTransferMsg, Nonce, OmniAddress, TransferId, TransferMessage, - UpdateFee, + BridgeOnTransferMsg, ChainKind, EvmAddress, Fee, InitTransferMsg, Nonce, OmniAddress, + TransferId, TransferMessage, UpdateFee, }; use crate::storage::Decimals; @@ -95,7 +95,7 @@ fn run_ft_on_transfer( token_id: String, amount: U128, attached_deposit: Option, - msg: &InitTransferMsg, + msg: &BridgeOnTransferMsg, ) -> PromiseOrValue { let sender_id = AccountId::try_from(sender_id).expect("Invalid sender ID"); let token_id = AccountId::try_from(token_id).expect("Invalid token ID"); @@ -111,7 +111,7 @@ fn run_ft_on_transfer( run_storage_deposit(contract, sender_id.clone(), attached_deposit); setup_test_env(token_id.clone(), NearToken::from_yoctonear(0), None); - let msg = serde_json::to_string(&msg).expect("Failed to serialize transfer message"); + let msg = serde_json::to_string(msg).expect("Failed to serialize transfer message"); contract.ft_on_transfer(sender_id, amount, msg) } @@ -127,7 +127,7 @@ fn test_initialize_contract() { } #[test] -fn test_ft_on_transfer_nonce_increment() { +fn test_init_transfer_nonce_increment() { let mut contract = get_default_contract(); run_ft_on_transfer( @@ -136,14 +136,14 @@ fn test_ft_on_transfer_nonce_increment() { DEFAULT_FT_CONTRACT_ACCOUNT.to_string(), U128(100), None, - &get_init_transfer_msg(DEFAULT_ETH_USER_ADDRESS, 0, 0), + &BridgeOnTransferMsg::InitTransfer(get_init_transfer_msg(&DEFAULT_ETH_USER_ADDRESS, 0, 0)), ); assert_eq!(contract.current_origin_nonce, DEFAULT_NONCE + 1); } #[test] -fn test_ft_on_transfer_stored_transfer_message() { +fn test_init_transfer_stored_transfer_message() { let mut contract = get_default_contract(); let msg = get_init_transfer_msg(DEFAULT_ETH_USER_ADDRESS, 0, 0); @@ -153,7 +153,7 @@ fn test_ft_on_transfer_stored_transfer_message() { DEFAULT_FT_CONTRACT_ACCOUNT.to_string(), U128(DEFAULT_TRANSFER_AMOUNT), None, - &msg, + &BridgeOnTransferMsg::InitTransfer(msg.clone()), ); let stored_transfer = contract.get_transfer_message(TransferId { @@ -187,7 +187,7 @@ fn test_ft_on_transfer_stored_transfer_message() { } #[test] -fn test_ft_on_transfer_promise_result() { +fn test_init_transfer_promise_result() { let mut contract = get_default_contract(); let promise = run_ft_on_transfer( @@ -196,7 +196,7 @@ fn test_ft_on_transfer_promise_result() { DEFAULT_FT_CONTRACT_ACCOUNT.to_string(), U128(DEFAULT_TRANSFER_AMOUNT), None, - &get_init_transfer_msg(DEFAULT_ETH_USER_ADDRESS, 0, 0), + &BridgeOnTransferMsg::InitTransfer(get_init_transfer_msg(&DEFAULT_ETH_USER_ADDRESS, 0, 0)), ); let remaining = match promise { @@ -208,7 +208,7 @@ fn test_ft_on_transfer_promise_result() { #[test] #[should_panic(expected = "ERR_INVALID_FEE")] -fn test_ft_on_transfer_invalid_fee() { +fn test_init_transfer_invalid_fee() { let mut contract = get_default_contract(); run_ft_on_transfer( &mut contract, @@ -216,12 +216,16 @@ fn test_ft_on_transfer_invalid_fee() { DEFAULT_FT_CONTRACT_ACCOUNT.to_string(), U128(DEFAULT_TRANSFER_AMOUNT), None, - &get_init_transfer_msg(DEFAULT_ETH_USER_ADDRESS, DEFAULT_TRANSFER_AMOUNT + 1, 0), + &BridgeOnTransferMsg::InitTransfer(get_init_transfer_msg( + &DEFAULT_ETH_USER_ADDRESS, + DEFAULT_TRANSFER_AMOUNT + 1, + 0, + )), ); } #[test] -fn test_ft_on_transfer_balance_updated() { +fn test_init_transfer_balance_updated() { let mut contract = get_default_contract(); let min_storage_balance = contract.required_balance_for_account(); @@ -234,7 +238,7 @@ fn test_ft_on_transfer_balance_updated() { DEFAULT_FT_CONTRACT_ACCOUNT.to_string(), U128(DEFAULT_TRANSFER_AMOUNT), Some(total_balance), - &get_init_transfer_msg(DEFAULT_ETH_USER_ADDRESS, 0, 0), + &BridgeOnTransferMsg::InitTransfer(get_init_transfer_msg(&DEFAULT_ETH_USER_ADDRESS, 0, 0)), ); let storage_balance = contract @@ -267,6 +271,7 @@ fn run_update_transfer_fee( sender: OmniAddress::Near(sender_id.clone().parse().unwrap()), msg: String::new(), destination_nonce: 1, + origin_transfer_id: None, }; contract.insert_raw_transfer( diff --git a/near/omni-tests/src/fast_transfer.rs b/near/omni-tests/src/fast_transfer.rs new file mode 100644 index 00000000..c00ba203 --- /dev/null +++ b/near/omni-tests/src/fast_transfer.rs @@ -0,0 +1,1013 @@ +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use near_sdk::{ + borsh, + json_types::U128, + serde_json::{self, json}, + AccountId, + }; + use near_workspaces::{ + result::{ExecutionResult, Value}, + types::NearToken, + }; + use omni_types::{ + locker_args::{FinTransferArgs, StorageDepositAction}, + prover_result::{InitTransferMessage, ProverResult}, + BasicMetadata, BridgeOnTransferMsg, ChainKind, FastFinTransferMsg, Fee, OmniAddress, + TransferId, TransferMessage, + }; + + use crate::helpers::tests::{ + account_n, base_eoa_address, base_factory_address, eth_eoa_address, eth_factory_address, + eth_token_address, get_bind_token_args, locker_wasm, mock_prover_wasm, mock_token_wasm, + relayer_account_id, token_deployer_wasm, NEP141_DEPOSIT, + }; + + struct TestEnv { + token_contract: near_workspaces::Contract, + eth_token_address: OmniAddress, + bridge_contract: near_workspaces::Contract, + relayer_account: near_workspaces::Account, + } + + impl TestEnv { + async fn new_with_native_token() -> anyhow::Result { + Self::new(false).await + } + + async fn new_with_bridged_token() -> anyhow::Result { + Self::new(true).await + } + + async fn new(is_bridged_token: bool) -> anyhow::Result { + let sender_balance_token = 1_000_000; + let worker = near_workspaces::sandbox().await?; + + let prover_contract = worker.dev_deploy(&mock_prover_wasm()).await?; + // Deploy and initialize bridge + let bridge_contract = worker.dev_deploy(&locker_wasm()).await?; + bridge_contract + .call("new") + .args_json(json!({ + "prover_account": prover_contract.id(), + "mpc_signer": "mpc.testnet", + "nonce": U128(0), + "wnear_account_id": "wnear.testnet", + })) + .max_gas() + .transact() + .await? + .into_result()?; + + // Add ETH factory address to the bridge contract + let eth_factory_address = eth_factory_address(); + bridge_contract + .call("add_factory") + .args_json(json!({ + "address": eth_factory_address, + })) + .max_gas() + .transact() + .await? + .into_result()?; + + let base_factory_address = base_factory_address(); + bridge_contract + .call("add_factory") + .args_json(json!({ + "address": base_factory_address, + })) + .max_gas() + .transact() + .await? + .into_result()?; + + let token_deployer = worker + .create_tla_and_deploy( + account_n(1), + worker.dev_generate().await.1, + &token_deployer_wasm(), + ) + .await? + .unwrap(); + + token_deployer + .call("new") + .args_json(json!({ + "controller": bridge_contract.id(), + "dao": AccountId::from_str("dao.near").unwrap(), + })) + .max_gas() + .transact() + .await? + .into_result()?; + + bridge_contract + .call("add_token_deployer") + .args_json(json!({ + "chain": eth_factory_address.get_chain(), + "account_id": token_deployer.id(), + })) + .max_gas() + .transact() + .await? + .into_result()?; + + // Create relayer account. (Default account in sandbox has 100 NEAR) + let relayer_account = worker + .create_tla(relayer_account_id(), worker.dev_generate().await.1) + .await? + .unwrap(); + + let (token_contract, eth_token_address) = if is_bridged_token { + let (token_contract, eth_token_address) = + Self::deploy_bridged_token(&worker, &bridge_contract).await?; + + // Mint to relayer account + Self::fake_finalize_transfer( + &bridge_contract, + &token_contract, + eth_token_address.clone(), + &relayer_account, + eth_factory_address, + U128(sender_balance_token), + ) + .await?; + + // Register the bridge in the token contract + token_contract + .call("storage_deposit") + .args_json(json!({ + "account_id": bridge_contract.id(), + "registration_only": true, + })) + .deposit(NEP141_DEPOSIT) + .max_gas() + .transact() + .await? + .into_result()?; + + (token_contract, eth_token_address) + } else { + let (token_contract, eth_token_address) = + Self::deploy_native_token(worker, &bridge_contract, eth_factory_address) + .await?; + + // Register and send tokens to the relayer account + token_contract + .call("storage_deposit") + .args_json(json!({ + "account_id": relayer_account.id(), + "registration_only": true, + })) + .deposit(NEP141_DEPOSIT) + .max_gas() + .transact() + .await? + .into_result()?; + + token_contract + .call("ft_transfer") + .args_json(json!({ + "receiver_id": relayer_account.id(), + "amount": U128(sender_balance_token), + "memo": None::, + })) + .deposit(NearToken::from_yoctonear(1)) + .max_gas() + .transact() + .await? + .into_result()?; + + // Register and send tokens to the bridge contract + token_contract + .call("storage_deposit") + .args_json(json!({ + "account_id": bridge_contract.id(), + "registration_only": true, + })) + .deposit(NEP141_DEPOSIT) + .max_gas() + .transact() + .await? + .into_result()?; + + token_contract + .call("ft_transfer") + .args_json(json!({ + "receiver_id": bridge_contract.id(), + "amount": U128(sender_balance_token), + "memo": None::, + })) + .deposit(NearToken::from_yoctonear(1)) + .max_gas() + .transact() + .await? + .into_result()?; + + (token_contract, eth_token_address) + }; + + Ok(Self { + token_contract, + eth_token_address, + bridge_contract, + relayer_account, + }) + } + + async fn deploy_bridged_token( + worker: &near_workspaces::Worker, + bridge_contract: &near_workspaces::Contract, + ) -> anyhow::Result<(near_workspaces::Contract, OmniAddress)> { + let init_token_address = OmniAddress::new_zero(ChainKind::Eth).unwrap(); + let token_metadata = BasicMetadata { + name: "ETH from Ethereum".to_string(), + symbol: "ETH".to_string(), + decimals: 18, + }; + + let required_storage: NearToken = bridge_contract + .view("required_balance_for_deploy_token") + .await? + .json()?; + + bridge_contract + .call("deploy_native_token") + .args_json(json!({ + "chain_kind": init_token_address.get_chain(), + "name": token_metadata.name, + "symbol": token_metadata.symbol, + "decimals": token_metadata.decimals, + })) + .deposit(required_storage) + .max_gas() + .transact() + .await? + .into_result()?; + + let token_account_id: AccountId = bridge_contract + .view("get_token_id") + .args_json(json!({ + "address": init_token_address + })) + .await? + .json()?; + + let token_contract = worker + .import_contract(&token_account_id, worker) + .transact() + .await?; + + Ok((token_contract, init_token_address)) + } + + async fn deploy_native_token( + worker: near_workspaces::Worker, + bridge_contract: &near_workspaces::Contract, + eth_factory_address: OmniAddress, + ) -> Result<(near_workspaces::Contract, OmniAddress), anyhow::Error> { + let token_contract = worker.dev_deploy(&mock_token_wasm()).await?; + token_contract + .call("new_default_meta") + .args_json(json!({ + "owner_id": token_contract.id(), + "total_supply": U128(u128::MAX) + })) + .max_gas() + .transact() + .await? + .into_result()?; + let required_deposit_for_bind_token = bridge_contract + .view("required_balance_for_bind_token") + .await? + .json()?; + + bridge_contract + .call("bind_token") + .args_borsh(get_bind_token_args( + &token_contract.id(), + ð_token_address(), + ð_factory_address, + 18, + 18, + )) + .deposit(required_deposit_for_bind_token) + .max_gas() + .transact() + .await? + .into_result()?; + Ok((token_contract, eth_token_address())) + } + + async fn fake_finalize_transfer( + bridge_contract: &near_workspaces::Contract, + token_contract: &near_workspaces::Contract, + eth_token_address: OmniAddress, + recipient: &near_workspaces::Account, + emitter_address: OmniAddress, + amount: U128, + ) -> anyhow::Result<()> { + let storage_deposit_actions = vec![StorageDepositAction { + token_id: token_contract.id().clone(), + account_id: recipient.id().clone(), + storage_deposit_amount: Some(NEP141_DEPOSIT.as_yoctonear()), + }]; + let required_balance_for_fin_transfer: NearToken = bridge_contract + .view("required_balance_for_fin_transfer") + .await? + .json()?; + let required_deposit_for_fin_transfer = + NEP141_DEPOSIT.saturating_add(required_balance_for_fin_transfer); + + // Simulate finalization of transfer through locker + bridge_contract + .call("fin_transfer") + .args_borsh(FinTransferArgs { + chain_kind: ChainKind::Near, + storage_deposit_actions, + prover_args: borsh::to_vec(&ProverResult::InitTransfer(InitTransferMessage { + origin_nonce: 1, + token: eth_token_address, + recipient: OmniAddress::Near(recipient.id().clone()), + amount, + fee: Fee { + fee: U128(0), + native_fee: U128(0), + }, + sender: eth_eoa_address(), + msg: String::default(), + emitter_address, + }))?, + }) + .deposit(required_deposit_for_fin_transfer) + .max_gas() + .transact() + .await? + .into_result()?; + + Ok(()) + } + } + + async fn get_balance_required_for_fast_transfer_to_near( + bridge_contract: &near_workspaces::Contract, + is_storage_deposit: bool, + ) -> anyhow::Result { + let required_balance_for_account: NearToken = bridge_contract + .view("required_balance_for_account") + .await? + .json()?; + + let required_balance_fast_transfer: NearToken = bridge_contract + .view("required_balance_for_fast_transfer") + .await? + .json()?; + + let mut required_balance = + required_balance_for_account.saturating_add(required_balance_fast_transfer); + if is_storage_deposit { + required_balance = required_balance.saturating_add(NEP141_DEPOSIT); + } + + Ok(required_balance) + } + + async fn get_balance_required_for_fast_transfer_to_other_chain( + bridge_contract: &near_workspaces::Contract, + ) -> anyhow::Result { + let required_balance_for_account: NearToken = bridge_contract + .view("required_balance_for_account") + .await? + .json()?; + + let required_balance_fast_transfer: NearToken = bridge_contract + .view("required_balance_for_fast_transfer") + .await? + .json()?; + + let required_balance_init_transfer: NearToken = bridge_contract + .view("required_balance_for_init_transfer") + .await? + .json()?; + + Ok(required_balance_for_account + .saturating_add(required_balance_fast_transfer) + .saturating_add(required_balance_init_transfer)) + } + + async fn do_fast_transfer( + env: &TestEnv, + transfer_amount: u128, + fast_transfer_msg: FastFinTransferMsg, + ) -> anyhow::Result> { + let storage_deposit_amount = match fast_transfer_msg.recipient { + OmniAddress::Near(_) => { + get_balance_required_for_fast_transfer_to_near(&env.bridge_contract, true).await? + } + _ => { + get_balance_required_for_fast_transfer_to_other_chain(&env.bridge_contract).await? + } + }; + + // Deposit to the storage + env.relayer_account + .call(env.bridge_contract.id(), "storage_deposit") + .args_json(json!({ + "account_id": env.relayer_account.id(), + })) + .deposit(storage_deposit_amount) + .max_gas() + .transact() + .await? + .into_result()?; + + // Initiate the fast transfer + let transfer_result = env + .relayer_account + .call(env.token_contract.id(), "ft_transfer_call") + .args_json(json!({ + "receiver_id": env.bridge_contract.id(), + "amount": U128(transfer_amount), + "memo": None::, + "msg": serde_json::to_string(&BridgeOnTransferMsg::FastFinTransfer(fast_transfer_msg))?, + })) + .deposit(NearToken::from_yoctonear(1)) + .max_gas() + .transact() + .await? + .into_result()?; + + Ok(transfer_result) + } + + async fn do_fin_transfer( + env: &TestEnv, + transfer_msg: InitTransferMessage, + ) -> anyhow::Result> { + let required_balance_for_fin_transfer: NearToken = env + .bridge_contract + .view("required_balance_for_fin_transfer") + .await? + .json()?; + + let required_balance_for_init_transfer: NearToken = env + .bridge_contract + .view("required_balance_for_init_transfer") + .await? + .json()?; + + let attached_deposit = required_balance_for_init_transfer + .saturating_add(required_balance_for_fin_transfer) + .saturating_add(NEP141_DEPOSIT); + + let storage_deposit_action = StorageDepositAction { + token_id: env.token_contract.id().clone(), + account_id: env.relayer_account.id().clone(), + storage_deposit_amount: None, + }; + + let result = env + .relayer_account + .call(env.bridge_contract.id(), "fin_transfer") + .args_borsh(FinTransferArgs { + chain_kind: omni_types::ChainKind::Eth, + storage_deposit_actions: vec![storage_deposit_action], + prover_args: borsh::to_vec(&ProverResult::InitTransfer(transfer_msg)).unwrap(), + }) + .deposit(attached_deposit) + .max_gas() + .transact() + .await? + .into_result()?; + + Ok(result) + } + + async fn get_balance( + token_contract: &near_workspaces::Contract, + account_id: &AccountId, + ) -> anyhow::Result { + let balance: U128 = token_contract + .view("ft_balance_of") + .args_json(json!({ + "account_id": account_id, + })) + .await? + .json()?; + + Ok(balance) + } + + #[tokio::test] + async fn test_fast_transfer_to_near() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg); + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + + assert_eq!(0, result.failures().len()); + + let recipient_balance: U128 = get_balance(&env.token_contract, &account_n(1)).await?; + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(transfer_amount, recipient_balance.0); + assert_eq!(contract_balance_before, contract_balance_after); + assert_eq!( + relayer_balance_before, + U128(relayer_balance_after.0 + transfer_amount) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_near_bridged_token() -> anyhow::Result<()> { + let env = TestEnv::new_with_bridged_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg); + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(U128(0), contract_balance_before); + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + + assert_eq!(0, result.failures().len()); + + let recipient_balance: U128 = get_balance(&env.token_contract, &account_n(1)).await?; + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(transfer_amount, recipient_balance.0); + assert_eq!(U128(0), contract_balance_after); + assert_eq!( + relayer_balance_before, + U128(relayer_balance_after.0 + transfer_amount) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_near_bad_storage_deposit() -> anyhow::Result<()> { + let env = TestEnv::new_with_bridged_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let mut fast_transfer_msg = get_fast_transfer_msg(transfer_msg); + fast_transfer_msg.storage_deposit_amount = + Some(NEP141_DEPOSIT.saturating_mul(100).as_yoctonear()); + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(U128(0), contract_balance_before); + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + + assert_eq!(1, result.failures().len()); + let failure = result.failures()[0].clone().into_result(); + assert!(failure + .is_err_and(|err| { format!("{:?}", err).contains("Not enough storage deposited") })); + + let recipient_balance: U128 = get_balance(&env.token_contract, &account_n(1)).await?; + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(0, recipient_balance.0); + assert_eq!(U128(0), contract_balance_after); + assert_eq!(relayer_balance_before, relayer_balance_after); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_near_twice() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + let OmniAddress::Near(recipient) = fast_transfer_msg.recipient.clone() else { + panic!("Recipient is not a Near address"); + }; + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + let recipient_balance_before = get_balance(&env.token_contract, &recipient).await?; + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + assert_eq!(1, result.failures().len()); + + let failure = result.failures()[0].clone().into_result(); + assert!(failure.is_err_and(|err| { + format!("{:?}", err).contains("Fast transfer is already performed") + })); + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + let recipient_balance_after = get_balance(&env.token_contract, &recipient).await?; + + assert_eq!(relayer_balance_before, relayer_balance_after); + assert_eq!(contract_balance_before, contract_balance_after); + assert_eq!(recipient_balance_before, recipient_balance_after); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_near_twice_bridged_token() -> anyhow::Result<()> { + let env = TestEnv::new_with_bridged_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + let OmniAddress::Near(recipient) = fast_transfer_msg.recipient.clone() else { + panic!("Recipient is not a Near address"); + }; + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + let recipient_balance_before = get_balance(&env.token_contract, &recipient).await?; + + assert_eq!(U128(0), contract_balance_before); + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + assert!(result.failures().len() > 0); + + let failure = result.failures()[0].clone().into_result(); + assert!(failure.is_err_and(|err| { + format!("{:?}", err).contains("Fast transfer is already performed") + })); + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + let recipient_balance_after = get_balance(&env.token_contract, &recipient).await?; + + assert_eq!(relayer_balance_before, relayer_balance_after); + assert_eq!(U128(0), contract_balance_after); + assert_eq!(recipient_balance_before, recipient_balance_after); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_near_finalisation() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let recipient_balance_before = get_balance(&env.token_contract, &account_n(1)).await?; + + do_fin_transfer(&env, transfer_msg).await?; + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let recipient_balance_after = get_balance(&env.token_contract, &account_n(1)).await?; + + assert_eq!( + transfer_amount, + relayer_balance_after.0 - relayer_balance_before.0 + ); + assert_eq!(recipient_balance_after, recipient_balance_before); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_near_finalisation_twice() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_near(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + do_fin_transfer(&env, transfer_msg.clone()).await?; + let result = do_fin_transfer(&env, transfer_msg).await; + + assert!(result.is_err_and(|err| { + println!("err: {:?}", err); + format!("{:?}", err).contains("The transfer is already finalised") + })); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_other_chain() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_other_chain(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + assert_eq!(0, result.failures().len()); + + //get_transfer_message + let transfer_message: TransferMessage = env + .bridge_contract + .view("get_transfer_message") + .args_json(json!({ + "transfer_id": TransferId { + origin_chain: ChainKind::Near, + origin_nonce: 1, + }, + })) + .await? + .json()?; + + assert_eq!( + OmniAddress::Near(env.token_contract.id().clone()), + transfer_message.token + ); + assert_eq!(transfer_amount, transfer_message.amount.0); + assert_eq!(fast_transfer_msg.recipient, transfer_message.recipient); + assert_eq!(fast_transfer_msg.fee, transfer_message.fee); + assert_eq!(fast_transfer_msg.msg, transfer_message.msg); + assert_eq!( + OmniAddress::Near(env.relayer_account.id().clone()), + transfer_message.sender + ); + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!( + contract_balance_before, + U128(contract_balance_after.0 - transfer_amount) + ); + assert_eq!( + relayer_balance_before, + U128(relayer_balance_after.0 + transfer_amount) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_other_chain_bridged_token() -> anyhow::Result<()> { + let env = TestEnv::new_with_bridged_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_other_chain(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(U128(0), contract_balance_before); + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + assert_eq!(0, result.failures().len()); + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(U128(0), contract_balance_after); + assert_eq!( + relayer_balance_before, + U128(relayer_balance_after.0 + transfer_amount) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_other_chain_twice() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_other_chain(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + + assert_eq!(1, result.failures().len()); + + let failure = result.failures()[0].clone().into_result(); + assert!(failure.is_err_and(|err| { + format!("{:?}", err).contains("Fast transfer is already performed") + })); + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(relayer_balance_before, relayer_balance_after); + assert_eq!(contract_balance_before, contract_balance_after); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_other_chain_finalisation() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_other_chain(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + + do_fin_transfer(&env, transfer_msg).await?; + + let transfer_message = env + .bridge_contract + .view("get_transfer_message") + .args_json(json!({ + "transfer_id": TransferId { + origin_chain: ChainKind::Base, + origin_nonce: 0, + }, + })) + .await; + + assert!(transfer_message + .is_err_and(|err| { format!("{:?}", err).contains("The transfer does not exist") })); + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + + assert_eq!( + transfer_amount, + relayer_balance_after.0 - relayer_balance_before.0 + ); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_other_chain_finalisation_twice() -> anyhow::Result<()> { + let env = TestEnv::new_with_native_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_other_chain(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + do_fast_transfer(&env, transfer_amount, fast_transfer_msg.clone()).await?; + + do_fin_transfer(&env, transfer_msg.clone()).await?; + let result = do_fin_transfer(&env, transfer_msg).await; + + assert!(result.is_err_and(|err| { + format!("{:?}", err).contains("The transfer is already finalised") + })); + + Ok(()) + } + + #[tokio::test] + async fn test_fast_transfer_to_other_chain_already_finalised() -> anyhow::Result<()> { + let env = TestEnv::new_with_bridged_token().await?; + + let transfer_amount = 100; + let transfer_msg = get_transfer_msg_to_other_chain(&env, transfer_amount); + let fast_transfer_msg = get_fast_transfer_msg(transfer_msg.clone()); + + do_fin_transfer(&env, transfer_msg).await?; + + let relayer_balance_before = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_before = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(U128(0), contract_balance_before); + + let result = do_fast_transfer(&env, transfer_amount, fast_transfer_msg).await?; + + let relayer_balance_after = + get_balance(&env.token_contract, env.relayer_account.id()).await?; + let contract_balance_after = + get_balance(&env.token_contract, env.bridge_contract.id()).await?; + + assert_eq!(relayer_balance_before, relayer_balance_after); + assert_eq!(U128(0), contract_balance_after); + + assert_eq!(1, result.failures().len()); + let failure = result.failures()[0].clone().into_result(); + assert!(failure + .is_err_and(|err| { format!("{:?}", err).contains("ERR_TRANSFER_ALREADY_FINALISED") })); + + Ok(()) + } + + fn get_transfer_msg_to_near(env: &TestEnv, amount: u128) -> InitTransferMessage { + InitTransferMessage { + origin_nonce: 0, + token: env.eth_token_address.clone(), + recipient: OmniAddress::Near(account_n(1)), + amount: U128(amount), + fee: Fee { + fee: U128(0), + native_fee: U128(0), + }, + sender: eth_eoa_address(), + msg: String::default(), + emitter_address: eth_factory_address(), + } + } + + fn get_transfer_msg_to_other_chain(env: &TestEnv, amount: u128) -> InitTransferMessage { + InitTransferMessage { + origin_nonce: 0, + token: env.eth_token_address.clone(), + recipient: base_eoa_address(), + amount: U128(amount), + fee: Fee { + fee: U128(0), + native_fee: U128(0), + }, + sender: eth_eoa_address(), + msg: String::default(), + emitter_address: eth_factory_address(), + } + } + + fn get_fast_transfer_msg(transfer_msg: InitTransferMessage) -> FastFinTransferMsg { + FastFinTransferMsg { + transfer_id: TransferId { + origin_chain: transfer_msg.sender.get_chain(), + origin_nonce: transfer_msg.origin_nonce, + }, + recipient: transfer_msg.recipient.clone(), + fee: transfer_msg.fee, + msg: transfer_msg.msg, + storage_deposit_amount: match transfer_msg.recipient.get_chain() { + ChainKind::Near => Some(NEP141_DEPOSIT.as_yoctonear()), + _ => None, + }, + } + } +} diff --git a/near/omni-tests/src/helpers.rs b/near/omni-tests/src/helpers.rs index bc0276b3..09cb6527 100644 --- a/near/omni-tests/src/helpers.rs +++ b/near/omni-tests/src/helpers.rs @@ -111,6 +111,12 @@ pub mod tests { .unwrap() } + pub fn base_eoa_address() -> OmniAddress { + "base:0xc5ed912ca6db7b41de4ef3632fa0a5641e42bf09" + .parse() + .unwrap() + } + pub fn base_token_address() -> OmniAddress { "base:0x1234567890123456789012345678901234567890" .parse() diff --git a/near/omni-tests/src/init_transfer.rs b/near/omni-tests/src/init_transfer.rs index b669f227..ae492d02 100644 --- a/near/omni-tests/src/init_transfer.rs +++ b/near/omni-tests/src/init_transfer.rs @@ -7,8 +7,8 @@ mod tests { }; use near_workspaces::{result::ExecutionSuccess, types::NearToken, AccountId}; use omni_types::{ - near_events::OmniBridgeEvent, ChainKind, Fee, InitTransferMsg, OmniAddress, TransferId, - TransferMessage, UpdateFee, + near_events::OmniBridgeEvent, BridgeOnTransferMsg, ChainKind, Fee, InitTransferMsg, + OmniAddress, TransferId, TransferMessage, UpdateFee, }; use rstest::rstest; @@ -212,7 +212,7 @@ mod tests { "receiver_id": env.locker_contract.id(), "amount": U128(transfer_amount), "memo": None::, - "msg": serde_json::to_string(&init_transfer_msg)?, + "msg": serde_json::to_string(&BridgeOnTransferMsg::InitTransfer(init_transfer_msg))?, })) .deposit(NearToken::from_yoctonear(1)) .max_gas() diff --git a/near/omni-tests/src/lib.rs b/near/omni-tests/src/lib.rs index 1717cf35..c69725c9 100644 --- a/near/omni-tests/src/lib.rs +++ b/near/omni-tests/src/lib.rs @@ -1,3 +1,4 @@ +mod fast_transfer; mod fin_transfer; mod helpers; mod init_transfer; diff --git a/near/omni-types/src/lib.rs b/near/omni-types/src/lib.rs index 8c7981dc..98d75db5 100644 --- a/near/omni-types/src/lib.rs +++ b/near/omni-types/src/lib.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use core::fmt; use core::str::FromStr; use hex::FromHex; @@ -379,6 +380,21 @@ impl<'de> Deserialize<'de> for OmniAddress { } } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum BridgeOnTransferMsg { + InitTransfer(InitTransferMsg), + FastFinTransfer(FastFinTransferMsg), +} + +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug, Clone)] +pub struct FastFinTransferMsg { + pub transfer_id: TransferId, + pub recipient: OmniAddress, + pub fee: Fee, + pub msg: String, + pub storage_deposit_amount: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct InitTransferMsg { pub recipient: OmniAddress, @@ -426,6 +442,7 @@ pub struct TransferMessage { pub sender: OmniAddress, pub msg: String, pub destination_nonce: Nonce, + pub origin_transfer_id: Option, } impl TransferMessage { @@ -503,3 +520,44 @@ pub struct BasicMetadata { pub symbol: String, pub decimals: u8, } + +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone)] +pub struct FastTransferId(pub [u8; 32]); + +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone)] +pub struct FastTransfer { + pub transfer_id: TransferId, + pub token_id: AccountId, + pub amount: U128, + pub fee: Fee, + pub recipient: OmniAddress, + pub msg: String, +} + +impl FastTransfer { + #[allow(clippy::missing_panics_doc)] + pub fn id(&self) -> FastTransferId { + FastTransferId(utils::keccak256(&borsh::to_vec(self).unwrap())) + } +} + +impl FastTransfer { + pub fn from_transfer(transfer: TransferMessage, token_id: AccountId) -> Self { + FastTransfer { + transfer_id: transfer.get_transfer_id(), + token_id, + amount: transfer.amount, + fee: transfer.fee, + recipient: transfer.recipient, + msg: transfer.msg, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub struct FastTransferStatus { + pub finalised: bool, + pub relayer: AccountId, +} diff --git a/near/omni-types/src/near_events.rs b/near/omni-types/src/near_events.rs index d24fbd1d..3c4eed45 100644 --- a/near/omni-types/src/near_events.rs +++ b/near/omni-types/src/near_events.rs @@ -2,7 +2,7 @@ use near_sdk::near; use near_sdk::serde_json::json; use crate::mpc_types::SignatureResponse; -use crate::{MetadataPayload, TransferMessage, TransferMessagePayload}; +use crate::{FastTransfer, MetadataPayload, TransferId, TransferMessage, TransferMessagePayload}; #[near(serializers=[json])] #[derive(Clone, Debug)] @@ -27,6 +27,10 @@ pub enum OmniBridgeEvent { ClaimFeeEvent { transfer_message: TransferMessage, }, + FastTransferEvent { + fast_transfer: FastTransfer, + new_transfer_id: Option, + }, } impl OmniBridgeEvent { diff --git a/near/omni-types/src/tests/lib_test.rs b/near/omni-types/src/tests/lib_test.rs index 6bd28f70..d8e02899 100644 --- a/near/omni-types/src/tests/lib_test.rs +++ b/near/omni-types/src/tests/lib_test.rs @@ -345,6 +345,7 @@ fn test_transfer_message_getters() { fee: Fee::default(), sender: OmniAddress::Eth(evm_addr.clone()), msg: String::new(), + origin_transfer_id: None, }, ChainKind::Eth, TransferId { @@ -363,6 +364,7 @@ fn test_transfer_message_getters() { fee: Fee::default(), sender: OmniAddress::Near("alice.near".parse().unwrap()), msg: String::new(), + origin_transfer_id: None, }, ChainKind::Near, TransferId { @@ -381,6 +383,7 @@ fn test_transfer_message_getters() { fee: Fee::default(), sender: OmniAddress::Sol("11111111111111111111111111111111".parse().unwrap()), msg: String::new(), + origin_transfer_id: None, }, ChainKind::Sol, TransferId {