diff --git a/near/omni-bridge/src/lib.rs b/near/omni-bridge/src/lib.rs index acfa64ffa..5db236d48 100644 --- a/near/omni-bridge/src/lib.rs +++ b/near/omni-bridge/src/lib.rs @@ -25,13 +25,18 @@ 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, H160, }; use std::str::FromStr; -use storage::{Decimals, TransferMessageStorage, TransferMessageStorageValue, NEP141_DEPOSIT}; +use storage::{ + Decimals, FastTransferStatusStorage, TransferMessageStorage, TransferMessageStorageValue, + NEP141_DEPOSIT, +}; mod errors; +mod migrate; mod storage; #[cfg(test)] @@ -59,6 +64,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"; @@ -75,6 +82,7 @@ enum StorageKey { DeployedTokens, DestinationNonces, TokenDecimals, + FastTransfers, } #[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] @@ -175,6 +183,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, @@ -197,64 +206,48 @@ 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(); - - // User has to pay for storage and we can't trust sender_id. - let signer_id = env::signer_account_id(); - - // Avoid extra storage read by verifying native fee before checking the role - if parsed_msg.native_token_fee.0 > 0 - && self.acl_has_role(Role::NativeFeeRestricted.into(), signer_id.clone()) - { - env::panic_str("ERR_ACCOUNT_RESTRICTED_FROM_USING_NATIVE_FEE"); - } - - 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) + .or_else(|_| serde_json::from_str(&msg).map(BridgeOnTransferMsg::InitTransfer)) + .sdk_expect("ERR_PARSE_MSG"); + + // We can't trust sender_id to pay for storage as it can be spoofed. + let storage_payer = env::signer_account_id(); + let promise_or_value = match parsed_msg { + BridgeOnTransferMsg::InitTransfer(init_transfer_msg) => { + PromiseOrValue::Value(self.init_transfer( + sender_id, + storage_payer, + token_id.clone(), + amount, + init_transfer_msg, + )) + } + BridgeOnTransferMsg::FastFinTransfer(fast_fin_transfer_msg) => self.fast_fin_transfer( + token_id.clone(), + amount, + storage_payer, + 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(), signer_id.clone()); - required_storage_balance = required_storage_balance - .saturating_add(NearToken::from_yoctonear(parsed_msg.native_token_fee.0)); - - self.update_storage_balance( - signer_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 + } + } } } @@ -271,6 +264,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), @@ -451,7 +445,9 @@ impl Contract { fee_recipient, }; - let payload = near_sdk::env::keccak256_array(&borsh::to_vec(&transfer_payload).unwrap()); + let payload = near_sdk::env::keccak256_array( + &borsh::to_vec(&transfer_payload).sdk_expect("ERR_BORSH"), + ); ext_signer::ext(self.mpc_signer.clone()) .with_static_gas(MPC_SIGNING_GAS) @@ -468,6 +464,66 @@ impl Contract { ) } + fn init_transfer( + &mut self, + sender_id: AccountId, + storage_payer: AccountId, + token_id: AccountId, + amount: U128, + init_transfer_msg: InitTransferMsg, + ) -> U128 { + // Avoid extra storage read by verifying native fee before checking the role + if init_transfer_msg.native_token_fee.0 > 0 + && self.acl_has_role(Role::NativeFeeRestricted.into(), storage_payer.clone()) + { + env::panic_str("ERR_ACCOUNT_RESTRICTED_FROM_USING_NATIVE_FEE"); + } + + 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), + 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), + 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(), storage_payer.clone()); + required_storage_balance = required_storage_balance.saturating_add( + NearToken::from_yoctonear(init_transfer_msg.native_token_fee.0), + ); + + self.update_storage_balance( + storage_payer, + 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, @@ -497,7 +553,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 { @@ -506,12 +562,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) @@ -558,6 +615,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() { @@ -569,11 +627,165 @@ 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, + token_id: AccountId, + amount: U128, + storage_payer: AccountId, + 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, + }; + + if let OmniAddress::Near(recipient) = fast_fin_transfer_msg.recipient { + let storage_deposit_amount = fast_fin_transfer_msg + .storage_deposit_amount + .map(|amount| amount.0) + .unwrap_or_default(); + if storage_deposit_amount > 0 { + self.update_storage_balance( + storage_payer.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 + .map(|amount| amount.0), + }; + 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, + storage_payer, + fast_fin_transfer_msg.relayer, + ), + ), + ) + } else { + self.fast_fin_transfer_to_other_chain( + &fast_transfer, + storage_payer, + fast_fin_transfer_msg.relayer, + ); + PromiseOrValue::Value(U128(0)) + } + } + + #[private] + pub fn fast_fin_transfer_to_near_callback( + &mut self, + #[serializer(borsh)] fast_transfer: &FastTransfer, + #[serializer(borsh)] storage_payer: AccountId, + #[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, storage_payer.clone()) + .saturating_add(ONE_YOCTO); + self.update_storage_balance( + storage_payer, + 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.clone(), + 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(), + ) + } + + fn fast_fin_transfer_to_other_chain( + &mut self, + fast_transfer: &FastTransfer, + storage_payer: AccountId, + 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(), storage_payer.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), + 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, storage_payer.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(storage_payer, required_balance, NearToken::from_near(0)); + } + #[private] pub fn near_withdraw_callback(&self, recipient: AccountId, amount: NearToken) -> Promise { match env::promise_result(0) { @@ -630,27 +842,31 @@ 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" + ); + + self.remove_fast_transfer(&fast_transfer.id()); + } 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(fee_recipient.clone()) .transfer(NearToken::from_yoctonear(message.fee.native_fee.0)); } else { - ext_token::ext(self.get_native_token_id(message.get_origin_chain())) + ext_token::ext(self.get_native_token_id(origin_chain)) .with_static_gas(MINT_TOKEN_GAS) .mint(fee_recipient.clone(), message.fee.native_fee, None); } @@ -659,11 +875,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( @@ -875,6 +1103,7 @@ impl Contract { sender: OmniAddress::Near(sender_id.clone()), msg: String::new(), destination_nonce, + origin_transfer_id: None, }; let required_storage_balance = @@ -1045,6 +1274,27 @@ impl Contract { self.destination_nonces.get(&chain_kind).unwrap_or_default() } + #[private] + pub fn resolve_transfer(&mut self) -> 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() } @@ -1092,6 +1342,25 @@ 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, msg, is_fast_transfer) = + match self.get_fast_transfer_status(&fast_transfer.id()) { + Some(status) => { + require!( + predecessor_account_id == status.relayer, + "ERR_FAST_TRANSFER_PERFORMED_BY_ANOTHER_RELAYER" + ); + require!(!status.finalised, "ERR_FAST_TRANSFER_ALREADY_FINALISED"); + (status.relayer, String::new(), true) + } + None => (recipient, transfer_message.msg.clone(), false), + }; + + if is_fast_transfer { + self.remove_fast_transfer(&fast_transfer.id()); + } + let mut storage_deposit_action_index: usize = 0; require!( Self::check_storage_balance_result( @@ -1107,50 +1376,7 @@ 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( - Self::ext(env::current_account_id()) - .with_static_gas(NEAR_WITHDRAW_CALLBACK_GAS) - .near_withdraw_callback( - recipient, - NearToken::from_yoctonear(amount_to_transfer.0), - ), - ) - } else if is_deployed_token { - let deposit = if transfer_message.msg.is_empty() { - NO_DEPOSIT - } 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(), - ) - }; + let mut promise = self.send_tokens(token.clone(), recipient, amount_to_transfer, &msg); if transfer_message.fee.fee.0 > 0 { require!( @@ -1224,16 +1450,41 @@ 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.get_fast_transfer_status(&fast_transfer.id()) { + Some(status) => { + require!( + predecessor_account_id == status.relayer, + "ERR_FAST_TRANSFER_PERFORMED_BY_ANOTHER_RELAYER" + ); + require!(!status.finalised, "ERR_FAST_TRANSFER_ALREADY_FINALISED"); + 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, + U128(transfer_message.amount.0 - transfer_message.fee.fee.0), + "", + ); + self.mark_fast_transfer_as_finalised(&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, @@ -1244,34 +1495,74 @@ 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: &str, ) -> 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( + Self::ext(env::current_account_id()) + .with_static_gas(NEAR_WITHDRAW_CALLBACK_GAS) + .near_withdraw_callback(recipient, 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) + .with_attached_deposit(deposit) + .with_static_gas(MINT_TOKEN_GAS.saturating_add(FT_TRANSFER_CALL_GAS)) + .mint( + recipient, + amount, + (!msg.is_empty()).then(|| msg.to_string()), + ) + } else if msg.is_empty() { + ext_token::ext(token) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(FT_TRANSFER_GAS) + .ft_transfer(recipient, amount, None) + } else { + ext_token::ext(token) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(FT_TRANSFER_CALL_GAS) + .ft_transfer_call(recipient, amount, None, msg.to_string()) } + } + + 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 { @@ -1353,6 +1644,73 @@ impl Contract { .saturating_mul((env::storage_usage().saturating_sub(storage_usage)).into()) } + fn add_fast_transfer( + &mut self, + fast_transfer: &FastTransfer, + relayer: AccountId, + storage_owner: AccountId, + ) -> NearToken { + let storage_usage = env::storage_usage(); + require!( + self.fast_transfers + .insert( + &fast_transfer.id(), + &FastTransferStatusStorage::V0(FastTransferStatus { + relayer, + storage_owner, + finalised: false, + }), + ) + .is_none(), + "Fast transfer is already performed" + ); + env::storage_byte_cost() + .saturating_mul((env::storage_usage().saturating_sub(storage_usage)).into()) + } + + fn get_fast_transfer_status( + &self, + fast_transfer_id: &FastTransferId, + ) -> Option { + self.fast_transfers + .get(fast_transfer_id) + .map(storage::FastTransferStatusStorage::into_main) + } + + pub fn is_fast_transfer_finalised(&self, fast_transfer_id: &FastTransferId) -> bool { + self.fast_transfers + .get(fast_transfer_id) + .map(storage::FastTransferStatusStorage::into_main) + .is_some_and(|status| status.finalised) + } + + fn mark_fast_transfer_as_finalised(&mut self, fast_transfer_id: &FastTransferId) { + let mut status = self + .get_fast_transfer_status(fast_transfer_id) + .sdk_expect("ERR_FAST_TRANSFER_NOT_FOUND"); + status.finalised = true; + self.fast_transfers + .insert(fast_transfer_id, &FastTransferStatusStorage::V0(status)); + } + + fn remove_fast_transfer(&mut self, fast_transfer_id: &FastTransferId) { + let storage_usage = env::storage_usage(); + let fast_transfer = self + .fast_transfers + .remove(fast_transfer_id) + .map(storage::FastTransferStatusStorage::into_main) + .sdk_expect("ERR_TRANSFER_NOT_EXIST"); + + let refund = + env::storage_byte_cost().saturating_mul((storage_usage - env::storage_usage()).into()); + + if let Some(mut storage) = self.accounts_balances.get(&fast_transfer.storage_owner) { + storage.available = storage.available.saturating_add(refund); + self.accounts_balances + .insert(&fast_transfer.storage_owner, &storage); + } + } + fn update_storage_balance( &mut self, account_id: AccountId, diff --git a/near/omni-bridge/src/migrate.rs b/near/omni-bridge/src/migrate.rs new file mode 100644 index 000000000..211f84f17 --- /dev/null +++ b/near/omni-bridge/src/migrate.rs @@ -0,0 +1,57 @@ +use crate::{ + storage::{Decimals, TransferMessageStorage}, + Contract, ContractExt, StorageKey, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use near_contract_standards::storage_management::StorageBalance; +use near_sdk::{ + collections::{LookupMap, LookupSet}, + env, near, AccountId, PanicOnDefault, +}; +use omni_types::{ChainKind, Nonce, OmniAddress, TransferId}; + +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct OldState { + pub prover_account: AccountId, + pub factories: LookupMap, + pub pending_transfers: LookupMap, + pub finalised_transfers: LookupSet, + pub token_id_to_address: LookupMap<(ChainKind, AccountId), OmniAddress>, + pub token_address_to_id: LookupMap, + pub token_decimals: LookupMap, + pub deployed_tokens: LookupSet, + pub token_deployer_accounts: LookupMap, + pub mpc_signer: AccountId, + pub current_origin_nonce: Nonce, + pub destination_nonces: LookupMap, + pub accounts_balances: LookupMap, + pub wnear_account_id: AccountId, +} + +#[near] +impl Contract { + #[private] + #[init(ignore_state)] + pub fn migrate() -> Self { + let old_state: OldState = env::state_read() + .unwrap_or_else(|| env::panic_str("Old state not found. Migration is not needed.")); + + Self { + prover_account: old_state.prover_account, + factories: old_state.factories, + pending_transfers: old_state.pending_transfers, + finalised_transfers: old_state.finalised_transfers, + fast_transfers: LookupMap::new(StorageKey::FastTransfers), + token_id_to_address: old_state.token_id_to_address, + token_address_to_id: old_state.token_address_to_id, + token_decimals: old_state.token_decimals, + deployed_tokens: old_state.deployed_tokens, + token_deployer_accounts: old_state.token_deployer_accounts, + mpc_signer: old_state.mpc_signer, + current_origin_nonce: old_state.current_origin_nonce, + destination_nonces: old_state.destination_nonces, + accounts_balances: old_state.accounts_balances, + wnear_account_id: old_state.wnear_account_id, + } + } +} diff --git a/near/omni-bridge/src/storage.rs b/near/omni-bridge/src/storage.rs index 921153df5..a8c0716d0 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, TransferMessageV0}; use crate::{ require, ChainKind, Contract, ContractExt, Fee, OmniAddress, Promise, SdkExpect, @@ -11,6 +11,13 @@ use crate::{ pub const BRIDGE_TOKEN_INIT_BALANCE: NearToken = NearToken::from_near(3); pub const NEP141_DEPOSIT: NearToken = NearToken::from_yoctonear(1_250_000_000_000_000_000_000); +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone)] +pub struct TransferMessageStorageValueV0 { + pub message: TransferMessageV0, + pub owner: AccountId, +} + #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct TransferMessageStorageValue { @@ -22,13 +29,28 @@ pub struct TransferMessageStorageValue { #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub enum TransferMessageStorage { - V0(TransferMessageStorageValue), + V0(TransferMessageStorageValueV0), + V1(TransferMessageStorageValue), } impl TransferMessageStorage { pub fn into_main(self) -> TransferMessageStorageValue { match self { - TransferMessageStorage::V0(m) => m, + TransferMessageStorage::V0(m) => TransferMessageStorageValue { + message: TransferMessage { + origin_nonce: m.message.origin_nonce, + token: m.message.token, + amount: m.message.amount, + recipient: m.message.recipient, + fee: m.message.fee, + sender: m.message.sender, + msg: m.message.msg, + destination_nonce: m.message.destination_nonce, + origin_transfer_id: None, + }, + owner: m.owner, + }, + TransferMessageStorage::V1(m) => m, } } @@ -36,13 +58,27 @@ impl TransferMessageStorage { message: TransferMessage, owner: AccountId, ) -> Result, std::io::Error> { - borsh::to_vec(&TransferMessageStorage::V0(TransferMessageStorageValue { + borsh::to_vec(&TransferMessageStorage::V1(TransferMessageStorageValue { message, owner, })) } } +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone)] +pub enum FastTransferStatusStorage { + V0(FastTransferStatus), +} + +impl FastTransferStatusStorage { + pub fn into_main(self) -> FastTransferStatus { + match self { + FastTransferStatusStorage::V0(status) => status, + } + } +} + #[near(serializers=[borsh, json])] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Decimals { @@ -93,9 +129,13 @@ impl Contract { .sdk_expect("The amount is greater than the available storage balance"); self.accounts_balances.insert(&account_id, &storage); + + Promise::new(account_id).transfer(to_withdraw); + storage } + #[payable] pub fn storage_unregister(&mut self, force: Option) -> bool { assert_one_yocto(); let account_id = env::predecessor_account_id(); @@ -156,7 +196,7 @@ impl Contract { .sdk_expect("ERR_CAST"); let value_len: u64 = - borsh::to_vec(&TransferMessageStorage::V0(TransferMessageStorageValue { + borsh::to_vec(&TransferMessageStorage::V1(TransferMessageStorageValue { message: TransferMessage { origin_nonce: 0, token: OmniAddress::Near(max_account_id.clone()), @@ -166,6 +206,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 +223,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 +236,31 @@ impl Contract { storage_cost.saturating_add(ft_transfers_cost) } + pub fn required_balance_for_fast_transfer(&self) -> NearToken { + let key_len: u64 = borsh::to_vec(&[0u8; 32]) + .sdk_expect("ERR_BORSH") + .len() + .try_into() + .sdk_expect("ERR_CAST"); + + let max_account_id: AccountId = "a".repeat(64).parse().sdk_expect("ERR_PARSE_ACCOUNT_ID"); + let value_len: u64 = borsh::to_vec(&FastTransferStatusStorage::V0(FastTransferStatus { + relayer: max_account_id.clone(), + finalised: false, + storage_owner: max_account_id, + })) + .sdk_expect("ERR_BORSH") + .len() + .try_into() + .sdk_expect("ERR_CAST"); + + 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 b5835431c..2eb9f29e8 100644 --- a/near/omni-bridge/src/tests/lib_test.rs +++ b/near/omni-bridge/src/tests/lib_test.rs @@ -12,8 +12,8 @@ use omni_types::{ locker_args::StorageDepositAction, prover_result::{InitTransferMessage, ProverResult}, sol_address::SolAddress, - ChainKind, EvmAddress, Fee, InitTransferMsg, Nonce, OmniAddress, TransferId, TransferMessage, - UpdateFee, + BridgeOnTransferMsg, ChainKind, EvmAddress, Fee, InitTransferMsg, Nonce, OmniAddress, + TransferId, TransferMessage, UpdateFee, }; use crate::storage::Decimals; @@ -92,6 +92,33 @@ fn get_init_transfer_msg(recipient: &str, fee: u128, native_token_fee: u128) -> } fn run_ft_on_transfer( + contract: &mut Contract, + sender_id: String, + token_id: String, + amount: U128, + attached_deposit: Option, + 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"); + + let attached_deposit = if let Some(deplosit) = attached_deposit { + deplosit + } else { + let min_storage_balance = contract.required_balance_for_account(); + let init_transfer_balance = contract.required_balance_for_init_transfer(); + min_storage_balance.saturating_add(init_transfer_balance) + }; + + 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"); + + contract.ft_on_transfer(sender_id, amount, msg) +} + +fn run_ft_on_transfer_legacy( contract: &mut Contract, sender_id: String, token_id: String, @@ -113,7 +140,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) } @@ -129,7 +156,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( @@ -138,14 +165,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); @@ -155,7 +182,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 { @@ -189,7 +216,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( @@ -198,7 +225,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 { @@ -210,7 +237,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, @@ -218,12 +245,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(); @@ -236,7 +267,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 @@ -270,6 +301,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( @@ -855,3 +887,17 @@ fn test_get_bridged_token() { "Failed to handle NEAR to NEAR resolution" ); } + +#[test] +fn test_legacy_ft_on_transfer() { + let mut contract = get_default_contract(); + + run_ft_on_transfer_legacy( + &mut contract, + DEFAULT_NEAR_USER_ACCOUNT.to_string(), + DEFAULT_FT_CONTRACT_ACCOUNT.to_string(), + U128(100), + None, + &get_init_transfer_msg(DEFAULT_ETH_USER_ADDRESS, 0, 0), + ); +} diff --git a/near/omni-tests/src/data/omni_bridge-0_2_6.wasm b/near/omni-tests/src/data/omni_bridge-0_2_6.wasm new file mode 100644 index 000000000..2a1c5c9c0 Binary files /dev/null and b/near/omni-tests/src/data/omni_bridge-0_2_6.wasm differ diff --git a/near/omni-tests/src/fast_transfer.rs b/near/omni-tests/src/fast_transfer.rs new file mode 100644 index 000000000..1005dec0a --- /dev/null +++ b/near/omni-tests/src/fast_transfer.rs @@ -0,0 +1,1079 @@ +#[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 + } + + #[allow(clippy::too_many_lines)] + 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) + } + + mod transfer_to_near { + use super::*; + + #[tokio::test] + async fn succeeds_with_native_token() -> 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(&env, 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 succeeds_with_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(&env, 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 fails_due_to_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(&env, transfer_msg); + fast_transfer_msg.storage_deposit_amount = + Some(U128(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 succeeds_with_different_amount() -> 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(&env, 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 transfer_amount = transfer_amount + 10; + 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!( + recipient_balance_before.0 + 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 fails_due_to_duplicate_transfer() -> 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(&env, 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 fails_due_to_duplicate_transfer_with_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(&env, 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().is_empty()); + + 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(()) + } + } + + mod finalisation_to_near { + use super::*; + + #[tokio::test] + async fn succeeds() -> 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(&env, 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 fails_due_to_duplicate_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(&env, 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(()) + } + } + + mod transfer_to_other_chain { + use super::*; + + #[tokio::test] + async fn succeeds_with_native_token() -> 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(&env, 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 succeeds_with_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(&env, 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 fails_due_to_duplicate_transfer() -> 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(&env, 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 fails_due_to_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(&env, 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(()) + } + } + + mod finalisation_to_other_chain { + use super::*; + + #[tokio::test] + async fn succeeds() -> 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(&env, 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 fails_due_to_duplicate_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(&env, 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(()) + } + } + + 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( + env: &TestEnv, + 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(U128(NEP141_DEPOSIT.as_yoctonear())), + _ => None, + }, + relayer: env.relayer_account.id().clone(), + } + } +} diff --git a/near/omni-tests/src/helpers.rs b/near/omni-tests/src/helpers.rs index 9c56e9cc5..990033196 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 6c6146e72..9e28e500b 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; @@ -22,6 +22,8 @@ mod tests { const EXPECTED_RELAYER_GAS_COST: NearToken = NearToken::from_yoctonear(1_500_000_000_000_000_000_000); + const PREV_LOCKER_WASM_FILEPATH: &str = "src/data/omni_bridge-0_2_6.wasm"; + struct TestEnv { worker: near_workspaces::Worker, token_contract: near_workspaces::Contract, @@ -177,6 +179,53 @@ mod tests { } } + async fn init_transfer_legacy( + env: &TestEnv, + transfer_amount: u128, + init_transfer_msg: InitTransferMsg, + ) -> anyhow::Result { + let storage_deposit_amount = get_balance_required_for_account( + &env.locker_contract, + &env.sender_account, + &init_transfer_msg, + None, + ) + .await?; + + // Storage deposit + env.sender_account + .call(env.locker_contract.id(), "storage_deposit") + .args_json(json!({ + "account_id": env.sender_account.id(), + })) + .deposit(storage_deposit_amount) + .max_gas() + .transact() + .await? + .into_result()?; + + // Initiate the transfer + let transfer_result = env + .sender_account + .call(env.token_contract.id(), "ft_transfer_call") + .args_json(json!({ + "receiver_id": env.locker_contract.id(), + "amount": U128(transfer_amount), + "memo": None::, + "msg": serde_json::to_string(&init_transfer_msg)?, + })) + .deposit(NearToken::from_yoctonear(1)) + .max_gas() + .transact() + .await? + .into_result()?; + + // Ensure the transfer event is emitted + let transfer_message = get_transfer_message_from_event(&transfer_result)?; + + Ok(transfer_message) + } + async fn init_transfer_flow_on_near( env: &TestEnv, transfer_amount: u128, @@ -213,7 +262,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() @@ -860,4 +909,80 @@ mod tests { .await .unwrap(); } + + #[rstest] + #[tokio::test] + async fn test_migrate( + mock_token_wasm: Vec, + mock_prover_wasm: Vec, + locker_wasm: Vec, + ) -> anyhow::Result<()> { + let sender_balance_token = 1_000_000; + let transfer_amount = 5000; + let init_transfer_msg = InitTransferMsg { + native_token_fee: U128(0), + fee: U128(0), + recipient: eth_eoa_address(), + }; + + let prev_locker_wasm = std::fs::read(PREV_LOCKER_WASM_FILEPATH).unwrap(); + let env = TestEnv::new( + sender_balance_token, + mock_token_wasm, + mock_prover_wasm, + prev_locker_wasm, + ) + .await?; + + let transfer_message = + init_transfer_legacy(&env, transfer_amount, init_transfer_msg.clone()).await?; + + let res = env + .locker_contract + .as_account() + .deploy(&locker_wasm) + .await + .unwrap(); + assert!(res.is_success(), "Failed to upgrade locker"); + + let res = env + .locker_contract + .call("migrate") + .args_json(json!({})) + .max_gas() + .transact() + .await?; + assert!(res.is_success(), "Migration didn't succeed"); + + let transfer = env + .locker_contract + .call("get_transfer_message") + .args_json(json!({ + "transfer_id": TransferId { + origin_chain: ChainKind::Near, + origin_nonce: transfer_message.origin_nonce, + }, + })) + .max_gas() + .transact() + .await?; + + let migrated_transfer = transfer.json::()?; + assert_eq!(migrated_transfer.origin_transfer_id, None); + assert_eq!( + migrated_transfer.origin_nonce, + transfer_message.origin_nonce + ); + assert_eq!(migrated_transfer.recipient, transfer_message.recipient); + assert_eq!(migrated_transfer.token, transfer_message.token); + assert_eq!(migrated_transfer.amount, transfer_message.amount); + assert_eq!(migrated_transfer.fee, transfer_message.fee); + assert_eq!(migrated_transfer.sender, transfer_message.sender); + assert_eq!( + migrated_transfer.destination_nonce, + transfer_message.destination_nonce + ); + + Ok(()) + } } diff --git a/near/omni-tests/src/lib.rs b/near/omni-tests/src/lib.rs index e53d4a38d..aeb06315e 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 faa189266..95c4c0063 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; @@ -397,6 +398,22 @@ 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, + pub relayer: AccountId, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct InitTransferMsg { pub recipient: OmniAddress, @@ -404,13 +421,6 @@ pub struct InitTransferMsg { pub native_token_fee: U128, } -#[near(serializers=[borsh, json])] -#[derive(Debug, Clone)] -pub struct FeeRecipient { - pub recipient: AccountId, - pub native_fee_recipient: OmniAddress, -} - #[near(serializers=[borsh, json])] #[derive(Debug, Clone, PartialEq, Default)] pub struct Fee { @@ -433,6 +443,19 @@ pub struct TransferId { pub origin_nonce: Nonce, } +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone)] +pub struct TransferMessageV0 { + pub origin_nonce: Nonce, + pub token: OmniAddress, + pub amount: U128, + pub recipient: OmniAddress, + pub fee: Fee, + pub sender: OmniAddress, + pub msg: String, + pub destination_nonce: Nonce, +} + #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct TransferMessage { @@ -444,6 +467,7 @@ pub struct TransferMessage { pub sender: OmniAddress, pub msg: String, pub destination_nonce: Nonce, + pub origin_transfer_id: Option, } impl TransferMessage { @@ -521,3 +545,46 @@ 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(near_sdk::env::sha256_array(&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, + } + } +} + +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone)] +pub struct FastTransferStatus { + pub finalised: bool, + pub relayer: AccountId, + pub storage_owner: AccountId, +} diff --git a/near/omni-types/src/near_events.rs b/near/omni-types/src/near_events.rs index 64829df1f..9088cf017 100644 --- a/near/omni-types/src/near_events.rs +++ b/near/omni-types/src/near_events.rs @@ -2,7 +2,10 @@ use near_sdk::serde_json::json; use near_sdk::{near, AccountId}; use crate::mpc_types::SignatureResponse; -use crate::{BasicMetadata, MetadataPayload, OmniAddress, TransferMessage, TransferMessagePayload}; +use crate::{ + BasicMetadata, FastTransfer, MetadataPayload, OmniAddress, TransferId, TransferMessage, + TransferMessagePayload, +}; #[near(serializers=[json])] #[derive(Clone, Debug)] @@ -38,6 +41,10 @@ pub enum OmniBridgeEvent { decimals: u8, origin_decimals: u8, }, + 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 6bd28f701..d8e02899e 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 {