diff --git a/api/gear/vft_manager.idl b/api/gear/vft_manager.idl index 7ce2ee84..ee08864c 100644 --- a/api/gear/vft_manager.idl +++ b/api/gear/vft_manager.idl @@ -1,79 +1,165 @@ +/// Config that should be provided to this service on initialization. type InitConfig = struct { + /// Address of the `ERC20Manager` contract on ethereum. + /// + /// For more info see [State::erc20_manager_address]. erc20_manager_address: h160, + /// Address of the gear-eth-bridge built-in actor. gear_bridge_builtin: actor_id, + /// Address of the `historical-proxy` program. + /// + /// For more info see [State::historical_proxy_address]. historical_proxy_address: actor_id, + /// Config that will be used to send messages to the other programs. + /// + /// For more info see [Config]. config: Config, }; +/// Config that will be used to send messages to the other programs. type Config = struct { + /// Gas limit for token operations. Token operations include: + /// - Mint + /// - Burn + /// - TransferFrom gas_for_token_ops: u64, + /// Gas to reserve for reply processing. gas_for_reply_deposit: u64, - gas_for_submit_receipt: u64, + /// Gas limit for gear-eth-bridge built-in actor request. gas_to_send_request_to_builtin: u64, + /// Timeout in blocks that current program will wait for reply from + /// the other programs such as `extended-vft` and `gear-eth-bridge` built-in actor. reply_timeout: u32, - gas_for_request_bridging: u64, }; +/// Error types for VFT Manageer service. type Error = enum { + /// Error sending message to the program. SendFailure, + /// Error while waiting for reply from the program. ReplyFailure, - BurnTokensDecode, - TransferFromDecode, - BuiltinDecode, - MintTokensDecode, + /// Failed to set reply timeout. ReplyTimeout, - NoCorrespondingEthAddress, + /// Failed to set reply hook. ReplyHook, + /// Original `MessageId` wasn't found in message tracker when processing reply. MessageNotFound, + /// Invalid message status was found in the message tracker when processing reply. InvalidMessageStatus, + /// Message sent to the program failed. MessageFailed, - BurnTokensFailed, - LockTokensFailed, - BridgeBuiltinMessageFailed, - TokensRefunded, - NotEthClient, - NotEnoughGas, + /// Failed to decode `extended-vft::Burn` reply. + BurnTokensDecode, + /// Failed to decode `extended-vft::TransferFrom` reply. + TransferFromDecode, + /// Failed to decode `extended-vft::Mint` reply. + MintTokensDecode, + /// Failed to decode payload from gear-eth-bridge built-in actor. + BuiltinDecode, + /// `ERC20` address wasn't found in the token mapping. + NoCorrespondingEthAddress, + /// `VFT` address wasn't found in the token mapping. NoCorrespondingVaraAddress, + /// `submit_receipt` can only be called by `historical-proxy` program. + NotHistoricalProxy, + /// Ethereum transaction receipt is not supported. NotSupportedEvent, - AlreadyProcessed, + /// Ethereum transaction is too old and already have been removed from storage. TransactionTooOld, + /// Ethereum transaction was already processed by VFT Manager service. + AlreadyProcessed, }; +/// Type of the token supply. type TokenSupply = enum { + /// Token supply is located on Ethereum. + /// + /// This means that we're working with some pre-existing `ERC20` token on Ethereum and with + /// wrapped `VFT` token on Gear. + /// + /// When this type of token supply is activated corresponding tokens will be minted/burned + /// on the gear side and locked/unlocked on the Ethereum side. + /// + /// For example this type of token supply can be used to work with + /// `USDT ERC20 token`/`wrappedUSDT VFT token` pair. Ethereum, + /// Token supply is located on Gear. + /// + /// This means that we're working with some pre-existing `VFT` token on Gear and with + /// wrapped `ERC20` token on Ethereum. + /// + /// When this type of token supply is activated corresponding tokens will be locked/unlocked + /// on the gear side and minted/burned on the Gear side. + /// + /// For example this type of token supply can be used to work with + /// `VARA VFT token`/`wrappedVARA ERC20 token` pair. Gear, }; -type MessageInfo = struct { - status: MessageStatus, - details: TxDetails, +/// Entry for a single message in [MessageTracker]. +type RequestBridgingMsgTrackerMessageInfo = struct { + /// State of the message. + status: RequestBridgingMsgTrackerMessageStatus, + /// Request details. + details: RequestBridgingMsgTrackerTxDetails, }; -type MessageStatus = enum { +/// State in which message processing can be. +type RequestBridgingMsgTrackerMessageStatus = enum { + /// Message to deposit tokens is sent. + SendingMessageToDepositTokens, + /// Reply is received for a token deposit message. + TokenDepositCompleted: bool, + /// Message to the `pallet-gear-eth-bridge` is sent. SendingMessageToBridgeBuiltin, + /// Reply is received for a message to the `pallet-gear-eth-bridge`. BridgeResponseReceived: opt u256, - WaitingReplyFromBuiltin, - BridgeBuiltinStep, - SendingMessageToBurnTokens, - TokenBurnCompleted: bool, - WaitingReplyFromBurn, - SendingMessageToMintTokens, - TokenMintCompleted, - WaitingReplyFromMint, - MintTokensStep, - SendingMessageToLockTokens, - TokenLockCompleted: bool, - WaitingReplyFromLock, - SendingMessageToUnlockTokens, - TokenUnlockCompleted, - WaitingReplyFromUnlock, - UnlockTokensStep, - MessageProcessedWithSuccess: u256, + /// Message to refund tokens is sent. + SendingMessageToReturnTokens, + /// Reply is received for a token refund message. + TokensReturnComplete: bool, +}; + +/// Details about a request associated with a message stored in [MessageTracker]. +type RequestBridgingMsgTrackerTxDetails = struct { + /// Address of the `VFT` token which is being bridged. + vara_token_id: actor_id, + /// Original `VFT` token owner. + sender: actor_id, + /// Bridged tokens amount. + amount: u256, + /// `ERC20` token receiver on Ethereum. + receiver: h160, + /// [TokenSupply] type of the token being bridged. + token_supply: TokenSupply, +}; + +/// Entry for a single message in [MessageTracker]. +type SubmitReceiptMsgTrackerMessageInfo = struct { + /// State of the message. + status: SubmitReceiptMsgTrackerMessageStatus, + /// Request details. + details: SubmitReceiptMsgTrackerTxDetails, +}; + +/// State in which message processing can be. +type SubmitReceiptMsgTrackerMessageStatus = enum { + /// Message to withdraw tokens is sent. + SendingMessageToWithdrawTokens, + /// Reply is received for a token withdraw message. + TokenWithdrawComplete: bool, }; -type TxDetails = enum { - RequestBridging: struct { vara_token_id: actor_id, sender: actor_id, amount: u256, receiver: h160 }, - SubmitReceipt: struct { vara_token_id: actor_id, receiver: actor_id, amount: u256 }, +/// Details about a request associated with a message stored in [MessageTracker]. +type SubmitReceiptMsgTrackerTxDetails = struct { + /// Address of the `VFT` token which is being bridged. + vara_token_id: actor_id, + /// Bridged tokens receiver on Gear. + receiver: actor_id, + /// Bridged tokens amount. + amount: u256, + /// [TokenSupply] type of the token being bridged. + token_supply: TokenSupply, }; constructor { @@ -81,31 +167,80 @@ constructor { }; service VftManager { - HandleInterruptedTransfer : (msg_id: message_id) -> result (struct { u256, h160 }, Error); + /// Process message further if some error was encountered during the `request_bridging`. + /// + /// This method should be called only to recover funds that were stuck in the middle of the bridging + /// and is not a part of a normal workflow. + /// + /// There can be several reasons for `request_bridging` to fail: + /// - Gas attached to a message wasn't enough to execute entire logic in `request_bridging`. + /// - Network was heavily loaded and some message was stuck so `request_bridging` failed. + HandleRequestBridgingInterruptedTransfer : (msg_id: message_id) -> result (null, Error); + /// Process message further if some error was encountered during the `submit_receipt`. + /// + /// This method should be called only to recover funds that were stuck in the middle of the bridging + /// and is not a part of a normal workflow. + /// + /// There can be several reasons for `submit_receipt` to fail: + /// - Gas attached to a message wasn't enough to execute entire logic in `submit_receipt`. + /// - Network was heavily loaded and some message was stuck so `submit_receipt` failed. + HandleSubmitReceiptInterruptedTransfer : (msg_id: message_id) -> result (null, Error); + /// Add a new token pair to a [State::token_map]. Can be called only by a [State::admin]. MapVaraToEthAddress : (vara_token_id: actor_id, eth_token_id: h160, supply_type: TokenSupply) -> null; + /// Remove the token pair from [State::token_map]. Can be called only by a [State::admin]. RemoveVaraToEthAddress : (vara_token_id: actor_id) -> null; - /// Request bridging of tokens from gear to ethereum. It involves locking/burning - /// `vft` tokens (specific operation depends on the token supply type) and sending - /// request to the bridge built-in actor. + /// Request bridging of tokens from Gear to Ethereum. + /// + /// Allowance should allow current program to spend `amount` tokens from the `sender` address. RequestBridging : (sender: actor_id, vara_token_id: actor_id, amount: u256, receiver: h160) -> result (struct { u256, h160 }, Error); - /// Submit rlp-encoded transaction receipt. This receipt is decoded under the hood - /// and checked that it's a valid receipt from tx send to `ERC20Manager` contract. - /// This entrypoint can be called only by `ethereum-event-client`. + /// Submit rlp-encoded transaction receipt. + /// + /// This receipt is decoded under the hood and checked that it's a valid receipt from tx + /// sent to `ERC20Manager` contract. + /// + /// This method can be called only by [State::historical_proxy_address] program. SubmitReceipt : (slot: u64, transaction_index: u64, receipt_rlp: vec u8) -> result (null, Error); + /// Change [Config]. Can be called only by a [State::admin]. + /// + /// For more info see [Config] docs. UpdateConfig : (config: Config) -> null; + /// Change [State::erc20_manager_address]. Can be called only by a [State::admin]. UpdateErc20ManagerAddress : (new_erc20_manager_address: h160) -> null; + /// Change [State::historical_proxy_address]. Can be called only by a [State::admin]. UpdateHistoricalProxyAddress : (historical_proxy_address_new: actor_id) -> null; + /// Get current [State::admin] address. query Admin : () -> actor_id; + /// Get current [State::erc20_manager_address] address. query Erc20ManagerAddress : () -> h160; + /// Get current [State::gear_bridge_builtin] address. query GearBridgeBuiltin : () -> actor_id; + /// Get current [Config]. query GetConfig : () -> Config; + /// Get current [State::historical_proxy_address]. query HistoricalProxyAddress : () -> actor_id; - query MsgTrackerState : () -> vec struct { message_id, MessageInfo }; + /// Get state of a `request_bridging` message tracker. + query RequestBridingMsgTrackerState : () -> vec struct { message_id, RequestBridgingMsgTrackerMessageInfo }; + /// Get state of a `submit_receipt` message tracker. + query SubmitReceiptMsgTrackerState : () -> vec struct { message_id, SubmitReceiptMsgTrackerMessageInfo }; + /// Get current [token mapping](State::token_map). query VaraToEthAddresses : () -> vec struct { actor_id, h160, TokenSupply }; events { + /// Token mapping was added. + /// + /// This means that VFT Manager service now supports specified + /// [vara_token_id](Event::TokenMappingAdded::vara_token_id)/[eth_token_id](Event::TokenMappingAdded::eth_token_id) pair. TokenMappingAdded: struct { vara_token_id: actor_id, eth_token_id: h160 }; + /// Token mapping was removed. + /// + /// This means that VFT Manager service doesn't support specified + /// [vara_token_id](Event::TokenMappingRemoved::vara_token_id)/[eth_token_id](Event::TokenMappingRemoved::eth_token_id) + /// pair anymore. TokenMappingRemoved: struct { vara_token_id: actor_id, eth_token_id: h160 }; + /// Bridging of tokens from Gear to Ethereum was requested. + /// + /// When this event is emitted it means that `VFT` tokens were locked/burned and + /// a message to the gear-eth-bridge built-in actor was successfully submitted. BridgingRequested: struct { nonce: u256, vara_token_id: actor_id, amount: u256, sender: actor_id, receiver: h160 }; } }; diff --git a/gear-programs/bridging-payment/tests/gtest.rs b/gear-programs/bridging-payment/tests/gtest.rs index 10f66103..1a05369f 100644 --- a/gear-programs/bridging-payment/tests/gtest.rs +++ b/gear-programs/bridging-payment/tests/gtest.rs @@ -91,10 +91,8 @@ async fn setup_for_test() -> Fixture { config: VftManagerConfig { gas_for_token_ops: 15_000_000_000, gas_for_reply_deposit: 15_000_000_000, - gas_for_submit_receipt: 20_000_000_000, gas_to_send_request_to_builtin: 15_000_000_000, reply_timeout: 100, - gas_for_request_bridging: 20_000_000_000, }, }; let vft_manager_program_id = VftManagerFactoryC::new(remoting.clone()) diff --git a/gear-programs/vft-manager/app/src/lib.rs b/gear-programs/vft-manager/app/src/lib.rs index 324a9bb8..a65b716a 100644 --- a/gear-programs/vft-manager/app/src/lib.rs +++ b/gear-programs/vft-manager/app/src/lib.rs @@ -1,6 +1,5 @@ #![no_std] -use collections::btree_set::BTreeSet; use sails_rs::{gstd::GStdExecContext, prelude::*}; pub mod services; use services::{InitConfig, VftManager}; @@ -11,9 +10,6 @@ pub struct Program; #[program] impl Program { pub fn new(init_config: InitConfig) -> Self { - unsafe { - services::TRANSACTIONS = Some(BTreeSet::new()); - } VftManager::::seed(init_config, GStdExecContext::new()); Self } diff --git a/gear-programs/vft-manager/app/src/services/bridge_builtin_operations.rs b/gear-programs/vft-manager/app/src/services/bridge_builtin_operations.rs deleted file mode 100644 index 12e1cc51..00000000 --- a/gear-programs/vft-manager/app/src/services/bridge_builtin_operations.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::{msg_tracker_mut, utils, Config, Error, MessageStatus}; -use gstd::MessageId; -use sails_rs::prelude::*; - -pub async fn send_message_to_bridge_builtin( - gear_bridge_builtin: ActorId, - receiver_contract_address: H160, - payload: Payload, - config: &Config, - msg_id: MessageId, -) -> Result { - msg_tracker_mut().update_message_status(msg_id, MessageStatus::SendingMessageToBridgeBuiltin); - - let payload_bytes = payload.pack(); - - let bytes = gbuiltin_eth_bridge::Request::SendEthMessage { - destination: receiver_contract_address, - payload: payload_bytes, - } - .encode(); - - utils::set_critical_hook(msg_id); - utils::send_message_with_gas_for_reply( - gear_bridge_builtin, - bytes, - config.gas_to_send_request_to_builtin, - config.gas_for_reply_deposit, - config.reply_timeout, - msg_id, - ) - .await?; - msg_tracker_mut().check_bridge_reply(&msg_id) -} - -#[derive(Debug, Decode, Encode, TypeInfo)] -pub struct Payload { - pub receiver: H160, - pub token_id: H160, - pub amount: U256, -} - -impl Payload { - pub fn pack(self) -> Vec { - // H160 is 20 bytes, U256 is 32 bytes - let mut packed = Vec::with_capacity(20 + 20 + 32); - - packed.extend_from_slice(self.receiver.as_bytes()); - packed.extend_from_slice(self.token_id.as_bytes()); - - let mut amount_bytes = [0u8; 32]; - self.amount.to_big_endian(&mut amount_bytes); - packed.extend_from_slice(&amount_bytes); - - packed - } -} diff --git a/gear-programs/vft-manager/app/src/services/error.rs b/gear-programs/vft-manager/app/src/services/error.rs index a75da71d..4550d4e4 100644 --- a/gear-programs/vft-manager/app/src/services/error.rs +++ b/gear-programs/vft-manager/app/src/services/error.rs @@ -1,27 +1,46 @@ use sails_rs::prelude::*; +/// Error types for VFT Manageer service. #[derive(Debug, Encode, Decode, TypeInfo, Clone, PartialEq, Eq)] pub enum Error { + /// Error sending message to the program. SendFailure, + /// Error while waiting for reply from the program. ReplyFailure, - BurnTokensDecode, - TransferFromDecode, - BuiltinDecode, - MintTokensDecode, + /// Failed to set reply timeout. ReplyTimeout, - NoCorrespondingEthAddress, + /// Failed to set reply hook. ReplyHook, + + /// Original `MessageId` wasn't found in message tracker when processing reply. MessageNotFound, + /// Invalid message status was found in the message tracker when processing reply. InvalidMessageStatus, + /// Message sent to the program failed. MessageFailed, - BurnTokensFailed, - LockTokensFailed, - BridgeBuiltinMessageFailed, - TokensRefunded, - NotEthClient, - NotEnoughGas, + + /// Failed to decode `extended-vft::Burn` reply. + BurnTokensDecode, + /// Failed to decode `extended-vft::TransferFrom` reply. + TransferFromDecode, + /// Failed to decode `extended-vft::Mint` reply. + MintTokensDecode, + + /// Failed to decode payload from gear-eth-bridge built-in actor. + BuiltinDecode, + + /// `ERC20` address wasn't found in the token mapping. + NoCorrespondingEthAddress, + /// `VFT` address wasn't found in the token mapping. NoCorrespondingVaraAddress, + + /// `submit_receipt` can only be called by `historical-proxy` program. + NotHistoricalProxy, + + /// Ethereum transaction receipt is not supported. NotSupportedEvent, - AlreadyProcessed, + /// Ethereum transaction is too old and already have been removed from storage. TransactionTooOld, + /// Ethereum transaction was already processed by VFT Manager service. + AlreadyProcessed, } diff --git a/gear-programs/vft-manager/app/src/services/mod.rs b/gear-programs/vft-manager/app/src/services/mod.rs index aae22de4..70a126e0 100644 --- a/gear-programs/vft-manager/app/src/services/mod.rs +++ b/gear-programs/vft-manager/app/src/services/mod.rs @@ -1,125 +1,181 @@ -use bridge_builtin_operations::Payload; -use collections::btree_set::BTreeSet; use sails_rs::{gstd::ExecContext, prelude::*}; -pub mod abi; -mod bridge_builtin_operations; -pub mod error; -pub mod msg_tracker; -mod utils; +mod error; +mod token_mapping; + use error::Error; -use msg_tracker::{MessageInfo, MessageStatus, MessageTracker, TxDetails}; use token_mapping::TokenMap; -mod token_mapping; -mod token_operations; -pub(crate) static mut TRANSACTIONS: Option> = None; -const CAPACITY: usize = 500_000; +mod request_bridging; +mod submit_receipt; -pub(crate) fn transactions_mut() -> &'static mut BTreeSet<(u64, u64)> { - unsafe { - TRANSACTIONS - .as_mut() - .expect("Program should be constructed") - } -} +pub use submit_receipt::abi as eth_abi; +/// VFT Manager service. pub struct VftManager { exec_context: ExecContext, } -#[derive(Debug, Decode, Encode, TypeInfo, Clone)] +/// Type of the token supply. +#[derive(Debug, Decode, Encode, TypeInfo, Clone, Copy)] #[repr(u8)] pub enum TokenSupply { + /// Token supply is located on Ethereum. + /// + /// This means that we're working with some pre-existing `ERC20` token on Ethereum and with + /// wrapped `VFT` token on Gear. + /// + /// When this type of token supply is activated corresponding tokens will be minted/burned + /// on the gear side and locked/unlocked on the Ethereum side. + /// + /// For example this type of token supply can be used to work with + /// `USDT ERC20 token`/`wrappedUSDT VFT token` pair. Ethereum = 0, + /// Token supply is located on Gear. + /// + /// This means that we're working with some pre-existing `VFT` token on Gear and with + /// wrapped `ERC20` token on Ethereum. + /// + /// When this type of token supply is activated corresponding tokens will be locked/unlocked + /// on the gear side and minted/burned on the Gear side. + /// + /// For example this type of token supply can be used to work with + /// `VARA VFT token`/`wrappedVARA ERC20 token` pair. Gear = 1, } +/// Events emitted by VFT Manager service. #[derive(Encode, TypeInfo)] #[codec(crate = sails_rs::scale_codec)] #[scale_info(crate = sails_rs::scale_info)] enum Event { + /// Token mapping was added. + /// + /// This means that VFT Manager service now supports specified + /// [vara_token_id](Event::TokenMappingAdded::vara_token_id)/[eth_token_id](Event::TokenMappingAdded::eth_token_id) pair. TokenMappingAdded { + /// `VFT` token address that was added into mapping. vara_token_id: ActorId, + /// `ERC20` token address that was added into mapping. eth_token_id: H160, }, + /// Token mapping was removed. + /// + /// This means that VFT Manager service doesn't support specified + /// [vara_token_id](Event::TokenMappingRemoved::vara_token_id)/[eth_token_id](Event::TokenMappingRemoved::eth_token_id) + /// pair anymore. TokenMappingRemoved { + /// `VFT` token address that was removed from mapping. vara_token_id: ActorId, + /// `ERC20` token address that was removed from mapping. eth_token_id: H160, }, + /// Bridging of tokens from Gear to Ethereum was requested. + /// + /// When this event is emitted it means that `VFT` tokens were locked/burned and + /// a message to the gear-eth-bridge built-in actor was successfully submitted. BridgingRequested { + /// Nonce that gear-eth-bridge built-in actor have returned. nonce: U256, + /// `VFT` token address that was locked/burned. vara_token_id: ActorId, + /// Amount of tokens that should be bridged. amount: U256, + /// Original token owner on the Gear side. sender: ActorId, + /// Receiver of the tokens on the Ethereum side. receiver: H160, }, } static mut STATE: Option = None; static mut CONFIG: Option = None; -static mut MSG_TRACKER: Option = None; +/// State of the VFT Manager service. #[derive(Debug, Default)] pub struct State { + /// Address of the gear-eth-bridge built-in actor. gear_bridge_builtin: ActorId, + /// Governance of this program. This address is in the charge of: + /// - Changing [Config] + /// - Updating [State::erc20_manager_address] + /// - Updating [State::historical_proxy_address] + /// - Managing token mapping in [State::token_map] admin: ActorId, + /// Address of the `ERC20Manager` contract address on Ethereum. + /// + /// Can be adjusted by the [State::admin]. erc20_manager_address: H160, + /// Mapping between `VFT` and `ERC20` tokens. + /// + /// Can be adjusted by the [State::admin]. token_map: TokenMap, + /// Address of the `historical-proxy` program. + /// + /// VFT Manager service will only accept incoming requests on token withdrawals + /// from this address. + /// + /// Can be adjusted by the [State::admin]. historical_proxy_address: ActorId, } +/// Config that should be provided to this service on initialization. #[derive(Debug, Decode, Encode, TypeInfo)] pub struct InitConfig { + /// Address of the `ERC20Manager` contract on ethereum. + /// + /// For more info see [State::erc20_manager_address]. pub erc20_manager_address: H160, + /// Address of the gear-eth-bridge built-in actor. pub gear_bridge_builtin: ActorId, + /// Address of the `historical-proxy` program. + /// + /// For more info see [State::historical_proxy_address]. pub historical_proxy_address: ActorId, + /// Config that will be used to send messages to the other programs. + /// + /// For more info see [Config]. pub config: Config, } -impl InitConfig { - pub fn new( - erc20_manager_address: H160, - gear_bridge_builtin: ActorId, - historical_proxy_address: ActorId, - config: Config, - ) -> Self { - Self { - erc20_manager_address, - gear_bridge_builtin, - historical_proxy_address, - config, - } - } -} - +/// Config that will be used to send messages to the other programs. #[derive(Debug, Decode, Encode, TypeInfo, Clone)] pub struct Config { + /// Gas limit for token operations. Token operations include: + /// - Mint + /// - Burn + /// - TransferFrom gas_for_token_ops: u64, + /// Gas to reserve for reply processing. gas_for_reply_deposit: u64, - gas_for_submit_receipt: u64, + /// Gas limit for gear-eth-bridge built-in actor request. gas_to_send_request_to_builtin: u64, + /// Timeout in blocks that current program will wait for reply from + /// the other programs such as `extended-vft` and `gear-eth-bridge` built-in actor. reply_timeout: u32, - gas_for_request_bridging: u64, } +/// VFT Manager service implementation. #[service(events = Event)] impl VftManager where T: ExecContext, { + /// Change [State::erc20_manager_address]. Can be called only by a [State::admin]. pub fn update_erc20_manager_address(&mut self, new_erc20_manager_address: H160) { self.ensure_admin(); self.state_mut().erc20_manager_address = new_erc20_manager_address; } + /// Change [State::historical_proxy_address]. Can be called only by a [State::admin]. pub fn update_historical_proxy_address(&mut self, historical_proxy_address_new: ActorId) { self.ensure_admin(); self.state_mut().historical_proxy_address = historical_proxy_address_new; } + /// Add a new token pair to a [State::token_map]. Can be called only by a [State::admin]. pub fn map_vara_to_eth_address( &mut self, vara_token_id: ActorId, @@ -139,6 +195,7 @@ where .expect("Failed to emit event"); } + /// Remove the token pair from [State::token_map]. Can be called only by a [State::admin]. pub fn remove_vara_to_eth_address(&mut self, vara_token_id: ActorId) { self.ensure_admin(); @@ -151,6 +208,9 @@ where .expect("Failed to emit event"); } + /// Change [Config]. Can be called only by a [State::admin]. + /// + /// For more info see [Config] docs. pub fn update_config(&mut self, config: Config) { self.ensure_admin(); @@ -159,118 +219,31 @@ where } } + /// Ensure that message sender is a [State::admin]. fn ensure_admin(&self) { if self.state().admin != self.exec_context.actor_id() { panic!("Not admin") } } - /// Submit rlp-encoded transaction receipt. This receipt is decoded under the hood - /// and checked that it's a valid receipt from tx send to `ERC20Manager` contract. - /// This entrypoint can be called only by `ethereum-event-client`. + /// Submit rlp-encoded transaction receipt. + /// + /// This receipt is decoded under the hood and checked that it's a valid receipt from tx + /// sent to `ERC20Manager` contract. + /// + /// This method can be called only by [State::historical_proxy_address] program. pub async fn submit_receipt( &mut self, slot: u64, transaction_index: u64, receipt_rlp: Vec, ) -> Result<(), Error> { - use alloy_rlp::Decodable; - use alloy_sol_types::SolEvent; - use ethereum_common::utils::ReceiptEnvelope; - - let state = self.state(); - let sender = self.exec_context.actor_id(); - - if sender != state.historical_proxy_address { - return Err(Error::NotEthClient); - } - - let config = self.config(); - if gstd::exec::gas_available() - < config.gas_for_token_ops - + config.gas_for_submit_receipt - + config.gas_for_reply_deposit - { - return Err(Error::NotEnoughGas); - } - - let receipt = - ReceiptEnvelope::decode(&mut &receipt_rlp[..]).map_err(|_| Error::NotSupportedEvent)?; - - if !receipt.is_success() { - return Err(Error::NotSupportedEvent); - } - - // decode log and check that it is from an allowed address - let (vara_token_id, event) = receipt - .logs() - .iter() - .find_map(|log| { - let address = H160::from(log.address.0 .0); - let event = - abi::ERC20_MANAGER::BridgingRequested::decode_log_data(log, true).ok()?; - let eth_token_id = H160::from(event.token.0 .0); - let vara_token_id = self - .state() - .token_map - .get_vara_token_id(ð_token_id) - .ok()?; - - (self.erc20_manager_address() == address).then_some((vara_token_id, event)) - }) - .ok_or(Error::NotSupportedEvent)?; - - let transactions = transactions_mut(); - let key = (slot, transaction_index); - if transactions.contains(&key) { - return Err(Error::AlreadyProcessed); - } - - if CAPACITY <= transactions.len() - && transactions - .first() - .map(|first| &key < first) - .unwrap_or(false) - { - return Err(Error::TransactionTooOld); - } - - let amount = U256::from_little_endian(event.amount.as_le_slice()); - let receiver = ActorId::from(event.to.0); - let msg_id = gstd::msg::id(); - let transaction_details = TxDetails::SubmitReceipt { - vara_token_id, - receiver, - amount, - }; - msg_tracker_mut().insert_message_info( - msg_id, - MessageStatus::SendingMessageToMintTokens, - transaction_details, - ); - utils::set_critical_hook(msg_id); - - let supply_type = self.state().token_map.get_supply_type(&vara_token_id)?; - - match supply_type { - TokenSupply::Ethereum => { - token_operations::mint(vara_token_id, receiver, amount, config, msg_id).await?; - } - TokenSupply::Gear => { - token_operations::unlock(vara_token_id, receiver, amount, config, msg_id).await?; - } - } - - if CAPACITY <= transactions.len() { - transactions.pop_first(); - } - transactions.insert((slot, transaction_index)); - Ok(()) + submit_receipt::submit_receipt(self, slot, transaction_index, receipt_rlp).await } - /// Request bridging of tokens from gear to ethereum. It involves locking/burning - /// `vft` tokens (specific operation depends on the token supply type) and sending - /// request to the bridge built-in actor. + /// Request bridging of tokens from Gear to Ethereum. + /// + /// Allowance should allow current program to spend `amount` tokens from the `sender` address. pub async fn request_bridging( &mut self, sender: ActorId, @@ -278,159 +251,79 @@ where amount: U256, receiver: H160, ) -> Result<(U256, H160), Error> { - let state = self.state(); - let msg_id = gstd::msg::id(); - let eth_token_id = self.state().token_map.get_eth_token_id(&vara_token_id)?; - let supply_type = self.state().token_map.get_supply_type(&vara_token_id)?; - let config = self.config(); - - if gstd::exec::gas_available() - < config.gas_for_token_ops - + config.gas_to_send_request_to_builtin - + config.gas_for_request_bridging - + 3 * config.gas_for_reply_deposit - { - panic!("Please attach more gas"); - } - - match supply_type { - TokenSupply::Ethereum => { - token_operations::burn(vara_token_id, sender, receiver, amount, config, msg_id) - .await?; - } - TokenSupply::Gear => { - token_operations::lock(vara_token_id, sender, amount, receiver, config, msg_id) - .await?; - } - } - - let payload = Payload { - receiver, - token_id: eth_token_id, - amount, - }; - let nonce = match bridge_builtin_operations::send_message_to_bridge_builtin( - state.gear_bridge_builtin, - state.erc20_manager_address, - payload, - config, - msg_id, - ) - .await - { - Ok(nonce) => nonce, - Err(e) => { - // In case of failure, mint tokens back to the sender - token_operations::mint(vara_token_id, sender, amount, config, msg_id).await?; - return Err(e); - } - }; - - self.notify_on(Event::BridgingRequested { - nonce, - vara_token_id, - amount, - sender, - receiver, - }) - .expect("Failed to emit event"); - - Ok((nonce, eth_token_id)) + request_bridging::request_bridging(self, sender, vara_token_id, amount, receiver).await } - pub async fn handle_interrupted_transfer( + /// Process message further if some error was encountered during the `request_bridging`. + /// + /// This method should be called only to recover funds that were stuck in the middle of the bridging + /// and is not a part of a normal workflow. + /// + /// There can be several reasons for `request_bridging` to fail: + /// - Gas attached to a message wasn't enough to execute entire logic in `request_bridging`. + /// - Network was heavily loaded and some message was stuck so `request_bridging` failed. + pub async fn handle_request_bridging_interrupted_transfer( &mut self, msg_id: MessageId, - ) -> Result<(U256, H160), Error> { - let state = self.state(); - - let config = self.config(); - let msg_tracker = msg_tracker_mut(); + ) -> Result<(), Error> { + request_bridging::handle_interrupted_transfer(self, msg_id).await + } - let msg_info = msg_tracker - .get_message_info(&msg_id) - .expect("Unexpected: msg status does not exist"); + /// Process message further if some error was encountered during the `submit_receipt`. + /// + /// This method should be called only to recover funds that were stuck in the middle of the bridging + /// and is not a part of a normal workflow. + /// + /// There can be several reasons for `submit_receipt` to fail: + /// - Gas attached to a message wasn't enough to execute entire logic in `submit_receipt`. + /// - Network was heavily loaded and some message was stuck so `submit_receipt` failed. + pub async fn handle_submit_receipt_interrupted_transfer( + &mut self, + msg_id: MessageId, + ) -> Result<(), Error> { + submit_receipt::handle_interrupted_transfer(self, msg_id).await + } - let TxDetails::RequestBridging { - vara_token_id, - sender, - amount, - receiver, - } = msg_info.details - else { - panic!("Wrong message type") - }; - - let eth_token_id = self - .state() - .token_map - .get_eth_token_id(&vara_token_id) - .expect("Failed to get ethereum token id"); - - match msg_info.status { - MessageStatus::TokenBurnCompleted(true) | MessageStatus::BridgeBuiltinStep => { - let payload = Payload { - receiver, - token_id: eth_token_id, - amount, - }; - - match bridge_builtin_operations::send_message_to_bridge_builtin( - state.gear_bridge_builtin, - state.erc20_manager_address, - payload, - config, - msg_id, - ) - .await - { - Ok(nonce) => Ok((nonce, eth_token_id)), - Err(_) => { - // In case of failure, mint tokens back to the sender - token_operations::mint(vara_token_id, sender, amount, config, msg_id) - .await?; - Err(Error::TokensRefunded) - } - } - } - MessageStatus::BridgeResponseReceived(Some(nonce)) => { - msg_tracker_mut().remove_message_info(&msg_id); - Ok((nonce, eth_token_id)) - } - MessageStatus::MintTokensStep => { - token_operations::mint(vara_token_id, sender, amount, config, msg_id).await?; - Err(Error::TokensRefunded) - } - _ => { - panic!("Unexpected status or transaction completed.") - } - } + /// Get state of a `request_bridging` message tracker. + pub fn request_briding_msg_tracker_state( + &self, + ) -> Vec<(MessageId, request_bridging::MsgTrackerMessageInfo)> { + request_bridging::msg_tracker_state() } - pub fn msg_tracker_state(&self) -> Vec<(MessageId, MessageInfo)> { - msg_tracker().message_info.clone().into_iter().collect() + /// Get state of a `submit_receipt` message tracker. + pub fn submit_receipt_msg_tracker_state( + &self, + ) -> Vec<(MessageId, submit_receipt::MsgTrackerMessageInfo)> { + submit_receipt::msg_tracker_state() } + /// Get current [token mapping](State::token_map). pub fn vara_to_eth_addresses(&self) -> Vec<(ActorId, H160, TokenSupply)> { self.state().token_map.read_state() } + /// Get current [State::erc20_manager_address] address. pub fn erc20_manager_address(&self) -> H160 { self.state().erc20_manager_address } + /// Get current [State::gear_bridge_builtin] address. pub fn gear_bridge_builtin(&self) -> ActorId { self.state().gear_bridge_builtin } + /// Get current [State::admin] address. pub fn admin(&self) -> ActorId { self.state().admin } + /// Get current [Config]. pub fn get_config(&self) -> Config { self.config().clone() } + /// Get current [State::historical_proxy_address]. pub fn historical_proxy_address(&self) -> ActorId { self.state().historical_proxy_address } @@ -440,6 +333,7 @@ impl VftManager where T: ExecContext, { + /// Initialize VFT Manager service. pub fn seed(config: InitConfig, exec_context: T) { unsafe { STATE = Some(State { @@ -450,22 +344,28 @@ where ..Default::default() }); CONFIG = Some(config.config); - MSG_TRACKER = Some(MessageTracker::default()); } + + request_bridging::seed(); + submit_receipt::seed(); } + /// Create VFT Manager service. pub fn new(exec_context: T) -> Self { Self { exec_context } } + /// Get a reference to the global [State]. fn state(&self) -> &State { unsafe { STATE.as_ref().expect("VftManager::seed() should be called") } } + /// Get a mutable reference to the global [State]. fn state_mut(&mut self) -> &mut State { unsafe { STATE.as_mut().expect("VftManager::seed() should be called") } } + /// Get a reference to the global [Config]. fn config(&self) -> &Config { unsafe { CONFIG @@ -474,19 +374,3 @@ where } } } - -fn msg_tracker() -> &'static MessageTracker { - unsafe { - MSG_TRACKER - .as_ref() - .expect("VftManager::seed() should be called") - } -} - -fn msg_tracker_mut() -> &'static mut MessageTracker { - unsafe { - MSG_TRACKER - .as_mut() - .expect("VftManager::seed() should be called") - } -} diff --git a/gear-programs/vft-manager/app/src/services/msg_tracker.rs b/gear-programs/vft-manager/app/src/services/msg_tracker.rs deleted file mode 100644 index 39e60a76..00000000 --- a/gear-programs/vft-manager/app/src/services/msg_tracker.rs +++ /dev/null @@ -1,159 +0,0 @@ -use super::Error; -use gstd::{prelude::collections::HashMap, MessageId}; -use sails_rs::prelude::*; - -#[derive(Default, Debug)] -pub struct MessageTracker { - pub message_info: HashMap, -} - -#[derive(Debug, Clone, Encode, Decode, TypeInfo)] -pub struct MessageInfo { - pub status: MessageStatus, - pub details: TxDetails, -} - -#[derive(Debug, Clone, Encode, Decode, TypeInfo)] -pub enum TxDetails { - RequestBridging { - vara_token_id: ActorId, - sender: ActorId, - amount: U256, - receiver: H160, - }, - SubmitReceipt { - vara_token_id: ActorId, - receiver: ActorId, - amount: U256, - }, -} - -impl MessageTracker { - pub fn insert_message_info( - &mut self, - msg_id: MessageId, - status: MessageStatus, - details: TxDetails, - ) { - self.message_info - .insert(msg_id, MessageInfo { status, details }); - } - - pub fn update_message_status(&mut self, msg_id: MessageId, status: MessageStatus) { - if let Some(info) = self.message_info.get_mut(&msg_id) { - info.status = status; - } - } - - pub fn get_message_info(&self, msg_id: &MessageId) -> Option<&MessageInfo> { - self.message_info.get(msg_id) - } - - pub fn remove_message_info(&mut self, msg_id: &MessageId) -> Option { - self.message_info.remove(msg_id) - } - - pub fn check_burn_result(&mut self, msg_id: &MessageId) -> Result<(), Error> { - if let Some(info) = self.message_info.get(msg_id) { - match info.status { - MessageStatus::TokenBurnCompleted(true) => Ok(()), - MessageStatus::TokenBurnCompleted(false) => { - self.message_info.remove(msg_id); - Err(Error::BurnTokensFailed) - } - _ => Err(Error::InvalidMessageStatus), - } - } else { - Err(Error::MessageNotFound) - } - } - - pub fn check_lock_result(&mut self, msg_id: &MessageId) -> Result<(), Error> { - if let Some(info) = self.message_info.get(msg_id) { - match info.status { - MessageStatus::TokenLockCompleted(true) => Ok(()), - MessageStatus::TokenLockCompleted(false) => { - self.message_info.remove(msg_id); - Err(Error::LockTokensFailed) - } - _ => Err(Error::InvalidMessageStatus), - } - } else { - Err(Error::MessageNotFound) - } - } - - pub fn check_mint_result(&mut self, msg_id: &MessageId) -> Result<(), Error> { - if let Some(info) = self.message_info.get(msg_id) { - match info.status { - MessageStatus::TokenMintCompleted => Ok(()), - MessageStatus::MintTokensStep => Err(Error::MessageFailed), - _ => Err(Error::InvalidMessageStatus), - } - } else { - Err(Error::MessageNotFound) - } - } - - pub fn check_unlock_result(&mut self, msg_id: &MessageId) -> Result<(), Error> { - if let Some(info) = self.message_info.get(msg_id) { - match info.status { - MessageStatus::TokenUnlockCompleted => Ok(()), - MessageStatus::UnlockTokensStep => Err(Error::MessageFailed), - _ => Err(Error::InvalidMessageStatus), - } - } else { - Err(Error::MessageNotFound) - } - } - - pub fn check_bridge_reply(&mut self, msg_id: &MessageId) -> Result { - if let Some(info) = self.message_info.get(msg_id) { - match info.status { - MessageStatus::BridgeResponseReceived(Some(nonce)) => { - self.remove_message_info(msg_id); - Ok(nonce) - } - MessageStatus::BridgeResponseReceived(None) => { - Err(Error::BridgeBuiltinMessageFailed) - } - _ => Err(Error::InvalidMessageStatus), - } - } else { - Err(Error::MessageNotFound) - } - } -} - -#[derive(Debug, Clone, PartialEq, Encode, Decode, TypeInfo)] -pub enum MessageStatus { - // Send message to bridge builtin - SendingMessageToBridgeBuiltin, - BridgeResponseReceived(Option), - WaitingReplyFromBuiltin, - BridgeBuiltinStep, - - // Burn tokens statuses - SendingMessageToBurnTokens, - TokenBurnCompleted(bool), - WaitingReplyFromBurn, - - // Mint tokens status - SendingMessageToMintTokens, - TokenMintCompleted, - WaitingReplyFromMint, - MintTokensStep, - - // Lock tokens statuses - SendingMessageToLockTokens, - TokenLockCompleted(bool), - WaitingReplyFromLock, - - // Unlock tokens status - SendingMessageToUnlockTokens, - TokenUnlockCompleted, - WaitingReplyFromUnlock, - UnlockTokensStep, - - MessageProcessedWithSuccess(U256), -} diff --git a/gear-programs/vft-manager/app/src/services/request_bridging/bridge_builtin_operations.rs b/gear-programs/vft-manager/app/src/services/request_bridging/bridge_builtin_operations.rs new file mode 100644 index 00000000..3c29dd5a --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/request_bridging/bridge_builtin_operations.rs @@ -0,0 +1,114 @@ +//! Operations involving comunication with `pallet-gear-eth-bridge` built-in actor. + +use gstd::{msg, MessageId}; +use sails_rs::prelude::*; + +use super::{ + super::{Config, Error}, + msg_tracker::{msg_tracker_mut, MessageStatus}, +}; + +/// Payload of the message that `ERC20Manager` will accept. +#[derive(Debug, Decode, Encode, TypeInfo)] +pub struct Payload { + /// Account of the tokens receiver. + pub receiver: H160, + /// Address of the bridged `ERC20` token contract. + pub token_id: H160, + /// Bridged amount. + pub amount: U256, +} + +impl Payload { + /// Pack [Payload] into a binary format that `ERC20Manager` will parse. + pub fn pack(self) -> Vec { + // H160 is 20 bytes, U256 is 32 bytes + let mut packed = Vec::with_capacity(20 + 20 + 32); + + packed.extend_from_slice(self.receiver.as_bytes()); + packed.extend_from_slice(self.token_id.as_bytes()); + + let mut amount_bytes = [0u8; 32]; + self.amount.to_big_endian(&mut amount_bytes); + packed.extend_from_slice(&amount_bytes); + + packed + } +} + +/// Send bridging request to a `pallet-gear-eth-bridge` built-in actor. +/// +/// It will asyncronously wait for reply from built-in and decode it +/// when it'll be received. +pub async fn send_message_to_bridge_builtin( + gear_bridge_builtin: ActorId, + erc20_manager: H160, + payload: Payload, + config: &Config, + msg_id: MessageId, +) -> Result { + let msg_tracker = msg_tracker_mut(); + + let payload_bytes = payload.pack(); + let bytes = gbuiltin_eth_bridge::Request::SendEthMessage { + destination: erc20_manager, + payload: payload_bytes, + } + .encode(); + + gstd::msg::send_bytes_with_gas_for_reply( + gear_bridge_builtin, + bytes, + config.gas_to_send_request_to_builtin, + 0, + config.gas_for_reply_deposit, + ) + .map_err(|_| Error::SendFailure)? + .up_to(Some(config.reply_timeout)) + .map_err(|_| Error::ReplyTimeout)? + .handle_reply(move || handle_reply_hook(msg_id)) + .map_err(|_| Error::ReplyHook)? + .await + .map_err(|_| Error::ReplyFailure)?; + + if let Some(info) = msg_tracker.get_message_info(&msg_id) { + match info.status { + MessageStatus::BridgeResponseReceived(Some(nonce)) => { + msg_tracker.remove_message_info(&msg_id); + Ok(nonce) + } + MessageStatus::BridgeResponseReceived(None) => Err(Error::MessageFailed), + _ => Err(Error::InvalidMessageStatus), + } + } else { + Err(Error::MessageNotFound) + } +} + +/// Handle reply received from `pallet-gear-eth-bridge` built-in actor. +/// +/// It will switch state of the currently processed message in +/// [message tracker](super::msg_tracker::MessageTracker) correspondingly. +fn handle_reply_hook(msg_id: MessageId) { + let msg_tracker = msg_tracker_mut(); + + let msg_info = msg_tracker + .get_message_info(&msg_id) + .expect("Unexpected: msg info does not exist"); + let reply_bytes = msg::load_bytes().expect("Unable to load bytes"); + + if msg_info.status == MessageStatus::SendingMessageToBridgeBuiltin { + let reply = decode_bridge_reply(&reply_bytes).ok().flatten(); + msg_tracker.update_message_status(msg_id, MessageStatus::BridgeResponseReceived(reply)); + } +} + +/// Decode reply received from `pallet-gear-eth-bridge` built-in actor. +fn decode_bridge_reply(mut bytes: &[u8]) -> Result, Error> { + let reply = + gbuiltin_eth_bridge::Response::decode(&mut bytes).map_err(|_| Error::BuiltinDecode)?; + + match reply { + gbuiltin_eth_bridge::Response::EthMessageQueued { nonce, .. } => Ok(Some(nonce)), + } +} diff --git a/gear-programs/vft-manager/app/src/services/request_bridging/mod.rs b/gear-programs/vft-manager/app/src/services/request_bridging/mod.rs new file mode 100644 index 00000000..c9a16c98 --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/request_bridging/mod.rs @@ -0,0 +1,170 @@ +//! Gear -> ethereum bridging request entrypoint of `VFTManager` service. + +use sails_rs::{gstd::ExecContext, prelude::*}; + +use super::{error::Error, Event, TokenSupply, VftManager}; + +mod bridge_builtin_operations; +mod msg_tracker; +mod token_operations; + +use bridge_builtin_operations::Payload; +use msg_tracker::{msg_tracker_mut, MessageStatus, TxDetails}; + +pub use msg_tracker::{msg_tracker_state, MessageInfo as MsgTrackerMessageInfo}; + +/// Initialize state that's used by this VFT Manager method. +pub fn seed() { + msg_tracker::init(); +} + +/// Lock/burn `vft` tokens (specific operation depends on the token supply type) and send +/// request to the bridge built-in actor. If request is failed then tokens will be refunded back +/// to the sender. +pub async fn request_bridging( + service: &mut VftManager, + sender: ActorId, + vara_token_id: ActorId, + amount: U256, + receiver: H160, +) -> Result<(U256, H160), Error> { + let state = service.state(); + let msg_id = gstd::msg::id(); + let eth_token_id = service.state().token_map.get_eth_token_id(&vara_token_id)?; + let supply_type = service.state().token_map.get_supply_type(&vara_token_id)?; + let config = service.config(); + + let transaction_details = TxDetails { + vara_token_id, + sender, + amount, + receiver, + token_supply: supply_type, + }; + + msg_tracker_mut().insert_message_info( + msg_id, + MessageStatus::SendingMessageToDepositTokens, + transaction_details, + ); + + match supply_type { + TokenSupply::Ethereum => { + token_operations::burn(vara_token_id, sender, amount, config, msg_id) + .await + .expect("Failed to burn tokens"); + } + TokenSupply::Gear => { + token_operations::lock(vara_token_id, sender, amount, config, msg_id) + .await + .expect("Failed to lock tokens"); + } + } + + let payload = Payload { + receiver, + token_id: eth_token_id, + amount, + }; + + msg_tracker_mut().update_message_status(msg_id, MessageStatus::SendingMessageToBridgeBuiltin); + + let bridge_builtin_reply = bridge_builtin_operations::send_message_to_bridge_builtin( + state.gear_bridge_builtin, + state.erc20_manager_address, + payload, + config, + msg_id, + ) + .await; + + let nonce = match bridge_builtin_reply { + Ok(nonce) => nonce, + Err(e) => { + msg_tracker_mut() + .update_message_status(msg_id, MessageStatus::SendingMessageToReturnTokens); + + match supply_type { + TokenSupply::Ethereum => { + token_operations::mint(vara_token_id, sender, amount, config, msg_id) + .await + .expect("Failed to mint tokens"); + } + TokenSupply::Gear => { + token_operations::unlock(vara_token_id, sender, amount, config, msg_id) + .await + .expect("Failed to unlock tokens"); + } + } + + return Err(e); + } + }; + + service + .notify_on(Event::BridgingRequested { + nonce, + vara_token_id, + amount, + sender, + receiver, + }) + .expect("Failed to emit event"); + + Ok((nonce, eth_token_id)) +} + +/// Try to execute failed request again. It can be used to return funds back to the user when +/// the [request_bridging] execution unexpectedly finished (due to the insufficient gas amount +/// or some other temporary error) but funds have already been locked/burnt. +/// +/// This function can return funds back to the user in the following scenarios: +/// - Token lock/burn is complete but message to the built-in actor haven't been sent yet. It can happen if +/// user haven't attached gas enough to process the message further after the first `wake` or if network +/// is loaded and timeout we've set to the reply is expired. +/// - Message to the built-in actor have returned error but token refund message haven't been sent yet. It +/// can happen if user haven't attached gas enough to process the message further after the second `wake` +/// or if network is loaded and timeout we've set to the reply is expired. +/// - Token refund message have been sent but it have failed. This case should be practically impossible +/// due to the invariants that `vft-manager` provides but left just in case. +pub async fn handle_interrupted_transfer( + service: &mut VftManager, + msg_id: MessageId, +) -> Result<(), Error> { + let config = service.config(); + + let msg_info = msg_tracker_mut() + .get_message_info(&msg_id) + .expect("Unexpected: msg status does not exist"); + + let TxDetails { + vara_token_id, + sender, + amount, + receiver: _, + token_supply, + } = msg_info.details; + + match msg_info.status { + MessageStatus::TokenDepositCompleted(true) + | MessageStatus::BridgeResponseReceived(None) + | MessageStatus::TokensReturnComplete(false) => { + msg_tracker_mut() + .update_message_status(msg_id, MessageStatus::SendingMessageToReturnTokens); + + match token_supply { + TokenSupply::Ethereum => { + token_operations::mint(vara_token_id, sender, amount, config, msg_id).await?; + } + TokenSupply::Gear => { + token_operations::unlock(vara_token_id, sender, amount, config, msg_id).await?; + } + } + + Ok(()) + } + _ => { + panic!("Unexpected status or transaction completed.") + } + } +} diff --git a/gear-programs/vft-manager/app/src/services/request_bridging/msg_tracker.rs b/gear-programs/vft-manager/app/src/services/request_bridging/msg_tracker.rs new file mode 100644 index 00000000..c344789a --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/request_bridging/msg_tracker.rs @@ -0,0 +1,114 @@ +use super::super::TokenSupply; +use gstd::{prelude::collections::HashMap, MessageId}; +use sails_rs::prelude::*; + +static mut MSG_TRACKER: Option = None; + +// TODO: Remove completed messages from tracker. +/// State machine which tracks state of each message that was submitted into +/// `request_bridging` method. +#[derive(Default, Debug)] +pub struct MessageTracker { + /// Message states. + pub message_info: HashMap, +} + +/// Entry for a single message in [MessageTracker]. +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +pub struct MessageInfo { + /// State of the message. + pub status: MessageStatus, + /// Request details. + pub details: TxDetails, +} + +/// Details about a request associated with a message stored in [MessageTracker]. +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +pub struct TxDetails { + /// Address of the `VFT` token which is being bridged. + pub vara_token_id: ActorId, + /// Original `VFT` token owner. + pub sender: ActorId, + /// Bridged tokens amount. + pub amount: U256, + /// `ERC20` token receiver on Ethereum. + pub receiver: H160, + /// [TokenSupply] type of the token being bridged. + pub token_supply: TokenSupply, +} + +/// State in which message processing can be. +#[derive(Debug, Clone, PartialEq, Encode, Decode, TypeInfo)] +pub enum MessageStatus { + /// Message to deposit tokens is sent. + SendingMessageToDepositTokens, + /// Reply is received for a token deposit message. + TokenDepositCompleted(bool), + + /// Message to the `pallet-gear-eth-bridge` is sent. + SendingMessageToBridgeBuiltin, + /// Reply is received for a message to the `pallet-gear-eth-bridge`. + BridgeResponseReceived(Option), + + /// Message to refund tokens is sent. + SendingMessageToReturnTokens, + /// Reply is received for a token refund message. + TokensReturnComplete(bool), +} + +/// Initialize global state of the message tracker. +pub fn init() { + unsafe { MSG_TRACKER = Some(MessageTracker::default()) } +} + +/// Fetch state of this message tracker. +pub fn msg_tracker_state() -> Vec<(MessageId, MessageInfo)> { + unsafe { + MSG_TRACKER + .as_mut() + .expect("VftManager::seed() should be called") + } + .message_info + .clone() + .into_iter() + .collect() +} + +/// Get mutable reference to a global message tracker. +pub fn msg_tracker_mut() -> &'static mut MessageTracker { + unsafe { + MSG_TRACKER + .as_mut() + .expect("VftManager::seed() should be called") + } +} + +impl MessageTracker { + /// Start tracking state of the message. + pub fn insert_message_info( + &mut self, + msg_id: MessageId, + status: MessageStatus, + details: TxDetails, + ) { + self.message_info + .insert(msg_id, MessageInfo { status, details }); + } + + /// Drive state machine further for a given `msg_id`. + pub fn update_message_status(&mut self, msg_id: MessageId, status: MessageStatus) { + if let Some(info) = self.message_info.get_mut(&msg_id) { + info.status = status; + } + } + + /// Get current state of the tracked message. Will return `None` if message isn't found. + pub fn get_message_info(&self, msg_id: &MessageId) -> Option<&MessageInfo> { + self.message_info.get(msg_id) + } + + /// Stop tracking message state. It will return current state of the target message. + pub fn remove_message_info(&mut self, msg_id: &MessageId) -> Option { + self.message_info.remove(msg_id) + } +} diff --git a/gear-programs/vft-manager/app/src/services/request_bridging/token_operations.rs b/gear-programs/vft-manager/app/src/services/request_bridging/token_operations.rs new file mode 100644 index 00000000..b2264a0b --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/request_bridging/token_operations.rs @@ -0,0 +1,233 @@ +use gstd::{msg, MessageId}; +use sails_rs::{calls::ActionIo, prelude::*}; + +use extended_vft_client::vft::io as vft_io; + +use crate::services::TokenSupply; + +use super::super::{Config, Error}; +use super::msg_tracker::{msg_tracker_mut, MessageStatus, MessageTracker}; + +/// Burn `amount` tokens from the `sender` address. +/// +/// It will send `Burn` call to the corresponding `VFT` program and +/// asyncronously wait for the reply. +pub async fn burn( + vara_token_id: ActorId, + sender: ActorId, + amount: U256, + config: &Config, + msg_id: MessageId, +) -> Result<(), Error> { + let bytes: Vec = vft_io::Burn::encode_call(sender, amount); + + send_message_with_gas_for_reply( + vara_token_id, + bytes, + config.gas_for_token_ops, + config.gas_for_reply_deposit, + config.reply_timeout, + msg_id, + ) + .await?; + + fetch_deposit_result(&*msg_tracker_mut(), &msg_id) +} + +/// Transfer `amount` tokens from the `sender` address to the current program address, +/// effectively locking them. +/// +/// It will send `TransferFrom` call to the corresponding `VFT` program and +/// asyncronously wait for the reply. +pub async fn lock( + vara_token_id: ActorId, + sender: ActorId, + amount: U256, + config: &Config, + msg_id: MessageId, +) -> Result<(), Error> { + let receiver = gstd::exec::program_id(); + let bytes: Vec = vft_io::TransferFrom::encode_call(sender, receiver, amount); + + send_message_with_gas_for_reply( + vara_token_id, + bytes, + config.gas_for_token_ops, + config.gas_for_reply_deposit, + config.reply_timeout, + msg_id, + ) + .await?; + + fetch_deposit_result(&*msg_tracker_mut(), &msg_id) +} + +/// Mint `amount` tokens into the `receiver` address. +/// +/// It will send `Mint` call to the corresponding `VFT` program and +/// asyncronously wait for the reply. +pub async fn mint( + token_id: ActorId, + receiver: ActorId, + amount: U256, + config: &Config, + msg_id: MessageId, +) -> Result<(), Error> { + let msg_tracker = msg_tracker_mut(); + + let bytes: Vec = vft_io::Mint::encode_call(receiver, amount); + send_message_with_gas_for_reply( + token_id, + bytes, + config.gas_for_token_ops, + config.gas_for_reply_deposit, + config.reply_timeout, + msg_id, + ) + .await?; + + fetch_withdraw_result(&*msg_tracker, &msg_id) +} + +/// Transfer `amount` tokens from the current program address to the `receiver` address, +/// effectively unlocking them. +/// +/// It will send `TransferFrom` call to the corresponding `VFT` program and +/// asyncronously wait for the reply. +pub async fn unlock( + vara_token_id: ActorId, + receiver: ActorId, + amount: U256, + config: &Config, + msg_id: MessageId, +) -> Result<(), Error> { + let msg_tracker = msg_tracker_mut(); + + let sender = gstd::exec::program_id(); + let bytes: Vec = vft_io::TransferFrom::encode_call(sender, receiver, amount); + + send_message_with_gas_for_reply( + vara_token_id, + bytes, + config.gas_for_token_ops, + config.gas_for_reply_deposit, + config.reply_timeout, + msg_id, + ) + .await?; + + fetch_withdraw_result(&*msg_tracker, &msg_id) +} + +/// Fetch result of the message sent to deposit tokens into this program. +/// +/// It will look for the specified [MessageId] in the [MessageTracker] and return result +/// based on this message state. The state should be present in the [MessageTracker] according +/// to the [handle_reply_hook] logic. +fn fetch_deposit_result(msg_tracker: &MessageTracker, msg_id: &MessageId) -> Result<(), Error> { + if let Some(info) = msg_tracker.message_info.get(msg_id) { + match info.status { + MessageStatus::TokenDepositCompleted(true) => Ok(()), + MessageStatus::TokenDepositCompleted(false) => Err(Error::MessageFailed), + _ => Err(Error::InvalidMessageStatus), + } + } else { + Err(Error::MessageNotFound) + } +} + +/// Fetch result of the message sent to withdraw tokens from this program. +/// +/// It will look for the specified [MessageId] in the [MessageTracker] and return result +/// based on this message state. The state should be present in the [MessageTracker] according +/// to the [handle_reply_hook] logic. +fn fetch_withdraw_result(msg_tracker: &MessageTracker, msg_id: &MessageId) -> Result<(), Error> { + if let Some(info) = msg_tracker.message_info.get(msg_id) { + match info.status { + MessageStatus::TokensReturnComplete(true) => Ok(()), + MessageStatus::TokensReturnComplete(false) => Err(Error::MessageFailed), + _ => Err(Error::InvalidMessageStatus), + } + } else { + Err(Error::MessageNotFound) + } +} + +/// Configure parameters for message sending and send message +/// asyncronously waiting for the reply. +/// +/// It will set reply hook to the [handle_reply_hook] and +/// timeout to the `reply_timeout`. +async fn send_message_with_gas_for_reply( + destination: ActorId, + message: Vec, + gas_to_send: u64, + gas_deposit: u64, + reply_timeout: u32, + msg_id: MessageId, +) -> Result<(), Error> { + gstd::msg::send_bytes_with_gas_for_reply(destination, message, gas_to_send, 0, gas_deposit) + .map_err(|_| Error::SendFailure)? + .up_to(Some(reply_timeout)) + .map_err(|_| Error::ReplyTimeout)? + .handle_reply(move || handle_reply_hook(msg_id)) + .map_err(|_| Error::ReplyHook)? + .await + .map_err(|_| Error::ReplyFailure)?; + + Ok(()) +} + +/// Handle reply received from `VFT` program. +/// +/// It will drive [MessageTracker] state machine further. +fn handle_reply_hook(msg_id: MessageId) { + let msg_tracker = msg_tracker_mut(); + + let msg_info = msg_tracker + .get_message_info(&msg_id) + .expect("Unexpected: msg info does not exist"); + let reply_bytes = msg::load_bytes().expect("Unable to load bytes"); + + match msg_info.status { + MessageStatus::SendingMessageToDepositTokens => { + let reply = match msg_info.details.token_supply { + TokenSupply::Ethereum => decode_burn_reply(&reply_bytes), + TokenSupply::Gear => decode_lock_reply(&reply_bytes), + } + .unwrap_or(false); + + msg_tracker.update_message_status(msg_id, MessageStatus::TokenDepositCompleted(reply)); + } + MessageStatus::SendingMessageToReturnTokens => { + let reply = match msg_info.details.token_supply { + TokenSupply::Ethereum => decode_mint_reply(&reply_bytes), + TokenSupply::Gear => decode_unlock_reply(&reply_bytes), + } + .unwrap_or(false); + + msg_tracker.update_message_status(msg_id, MessageStatus::TokensReturnComplete(reply)); + } + _ => {} + }; +} + +/// Decode reply received from the `extended-vft::Burn` method. +fn decode_burn_reply(bytes: &[u8]) -> Result { + vft_io::Burn::decode_reply(bytes).map_err(|_| Error::BurnTokensDecode) +} + +/// Decode reply received from the `extended-vft::TransferFrom` method. +fn decode_lock_reply(bytes: &[u8]) -> Result { + vft_io::TransferFrom::decode_reply(bytes).map_err(|_| Error::TransferFromDecode) +} + +/// Decode reply received from the `extended-vft::Mint` method. +fn decode_mint_reply(bytes: &[u8]) -> Result { + vft_io::Mint::decode_reply(bytes).map_err(|_| Error::MintTokensDecode) +} + +/// Decode reply received from the `extended-vft::TransferFrom` method. +fn decode_unlock_reply(bytes: &[u8]) -> Result { + vft_io::TransferFrom::decode_reply(bytes).map_err(|_| Error::TransferFromDecode) +} diff --git a/gear-programs/vft-manager/app/src/services/abi.rs b/gear-programs/vft-manager/app/src/services/submit_receipt/abi.rs similarity index 100% rename from gear-programs/vft-manager/app/src/services/abi.rs rename to gear-programs/vft-manager/app/src/services/submit_receipt/abi.rs diff --git a/gear-programs/vft-manager/app/src/services/submit_receipt/mod.rs b/gear-programs/vft-manager/app/src/services/submit_receipt/mod.rs new file mode 100644 index 00000000..fd845b8e --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/submit_receipt/mod.rs @@ -0,0 +1,188 @@ +use collections::btree_set::BTreeSet; +use sails_rs::{gstd::ExecContext, prelude::*}; + +use super::{error::Error, TokenSupply, VftManager}; + +pub mod abi; +mod msg_tracker; +mod token_operations; + +use msg_tracker::{msg_tracker_mut, MessageStatus, TxDetails}; + +pub use msg_tracker::{msg_tracker_state, MessageInfo as MsgTrackerMessageInfo}; + +/// Successfully processed Ethereum transactions. They're stored to prevent +/// double-spending attacks on this program. +static mut TRANSACTIONS: Option> = None; + +/// Maximum amount of successfully processed Ethereum transactions that this +/// program can store. +const TX_HISTORY_DEPTH: usize = 500_000; + +/// Get mutable reference to a transactions storage. +fn transactions_mut() -> &'static mut BTreeSet<(u64, u64)> { + unsafe { + TRANSACTIONS + .as_mut() + .expect("Program should be constructed") + } +} + +/// Initialize state that's used by this VFT Manager method. +pub fn seed() { + msg_tracker::init(); + + unsafe { + TRANSACTIONS = Some(BTreeSet::new()); + } +} + +/// Submit rlp-encoded transaction receipt. +/// +/// This receipt is decoded under the hood and checked that it's a valid receipt from tx +/// sent to `ERC20Manager` contract. Also it will check that this transaction haven't been +/// processed yet. +/// +/// This method can be called only by [State::historical_proxy_address] program. +pub async fn submit_receipt( + service: &mut VftManager, + slot: u64, + transaction_index: u64, + receipt_rlp: Vec, +) -> Result<(), Error> { + use alloy_rlp::Decodable; + use alloy_sol_types::SolEvent; + use ethereum_common::utils::ReceiptEnvelope; + + let state = service.state(); + let sender = service.exec_context.actor_id(); + + if sender != state.historical_proxy_address { + return Err(Error::NotHistoricalProxy); + } + + let receipt = + ReceiptEnvelope::decode(&mut &receipt_rlp[..]).map_err(|_| Error::NotSupportedEvent)?; + + if !receipt.is_success() { + return Err(Error::NotSupportedEvent); + } + + // Decode log and check that it is from an allowed address. + let (vara_token_id, event) = receipt + .logs() + .iter() + .find_map(|log| { + let address = H160::from(log.address.0 .0); + let event = abi::ERC20_MANAGER::BridgingRequested::decode_log_data(log, true).ok()?; + let eth_token_id = H160::from(event.token.0 .0); + let vara_token_id = service + .state() + .token_map + .get_vara_token_id(ð_token_id) + .ok()?; + + (service.erc20_manager_address() == address).then_some((vara_token_id, event)) + }) + .ok_or(Error::NotSupportedEvent)?; + + let transactions = transactions_mut(); + let key = (slot, transaction_index); + if transactions.contains(&key) { + return Err(Error::AlreadyProcessed); + } + + if transactions.len() >= TX_HISTORY_DEPTH + && transactions + .first() + .map(|first| &key < first) + .unwrap_or(false) + { + return Err(Error::TransactionTooOld); + } + + let msg_id = gstd::msg::id(); + let amount = U256::from_little_endian(event.amount.as_le_slice()); + let receiver = ActorId::from(event.to.0); + let supply_type = service.state().token_map.get_supply_type(&vara_token_id)?; + let transaction_details = TxDetails { + vara_token_id, + receiver, + amount, + token_supply: supply_type, + }; + + if transactions.len() >= TX_HISTORY_DEPTH { + transactions.pop_first(); + } + transactions.insert((slot, transaction_index)); + + msg_tracker_mut().insert_message_info( + msg_id, + MessageStatus::SendingMessageToWithdrawTokens, + transaction_details, + ); + + match supply_type { + TokenSupply::Ethereum => { + token_operations::mint(vara_token_id, receiver, amount, service.config(), msg_id) + .await + .expect("Failed to mint tokens"); + } + TokenSupply::Gear => { + token_operations::unlock(vara_token_id, receiver, amount, service.config(), msg_id) + .await + .expect("Failed to unlock tokens"); + } + } + + Ok(()) +} + +/// Try to execute failed request again. It can be used to complete funds withdrawal when +/// the [submit_receipt] execution unexpectedly finished (due to the insufficient gas amount +/// or some other temporary error) after message to `VFT` program have already been sent +/// and failed for some reason (in this case Ethereum transaction hash is already marked as processed). +pub async fn handle_interrupted_transfer( + service: &mut VftManager, + msg_id: MessageId, +) -> Result<(), Error> { + let config = service.config(); + let msg_tracker = msg_tracker_mut(); + + let msg_info = msg_tracker + .get_message_info(&msg_id) + .expect("Unexpected: msg status does not exist"); + + let TxDetails { + vara_token_id, + amount, + receiver, + token_supply, + } = msg_info.details; + + match msg_info.status { + MessageStatus::TokenWithdrawComplete(false) => { + msg_tracker_mut() + .update_message_status(msg_id, MessageStatus::SendingMessageToWithdrawTokens); + + match token_supply { + TokenSupply::Ethereum => { + token_operations::mint(vara_token_id, receiver, amount, config, msg_id) + .await + .expect("Failed to mint tokens"); + } + TokenSupply::Gear => { + token_operations::unlock(vara_token_id, receiver, amount, config, msg_id) + .await + .expect("Failed to unlock tokens"); + } + } + } + _ => { + panic!("Unexpected status or transaction completed.") + } + } + + Ok(()) +} diff --git a/gear-programs/vft-manager/app/src/services/submit_receipt/msg_tracker.rs b/gear-programs/vft-manager/app/src/services/submit_receipt/msg_tracker.rs new file mode 100644 index 00000000..07eb6550 --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/submit_receipt/msg_tracker.rs @@ -0,0 +1,102 @@ +use super::super::TokenSupply; +use gstd::{prelude::collections::HashMap, MessageId}; +use sails_rs::prelude::*; + +static mut MSG_TRACKER: Option = None; + +// TODO: remove completed messages. +/// State machine which tracks state of each message that was submitted into +/// `submit_receipt` method. +#[derive(Default, Debug)] +pub struct MessageTracker { + /// Message states. + pub message_info: HashMap, +} + +/// Entry for a single message in [MessageTracker]. +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +pub struct MessageInfo { + /// State of the message. + pub status: MessageStatus, + /// Request details. + pub details: TxDetails, +} + +/// Details about a request associated with a message stored in [MessageTracker]. +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +pub struct TxDetails { + /// Address of the `VFT` token which is being bridged. + pub vara_token_id: ActorId, + /// Bridged tokens receiver on Gear. + pub receiver: ActorId, + /// Bridged tokens amount. + pub amount: U256, + /// [TokenSupply] type of the token being bridged. + pub token_supply: TokenSupply, +} + +/// State in which message processing can be. +#[derive(Debug, Clone, PartialEq, Encode, Decode, TypeInfo)] +pub enum MessageStatus { + /// Message to withdraw tokens is sent. + SendingMessageToWithdrawTokens, + /// Reply is received for a token withdraw message. + TokenWithdrawComplete(bool), +} + +/// Initialize global state of the message tracker. +pub fn init() { + unsafe { MSG_TRACKER = Some(MessageTracker::default()) } +} + +/// Fetch state of this message tracker. +pub fn msg_tracker_state() -> Vec<(MessageId, MessageInfo)> { + unsafe { + MSG_TRACKER + .as_mut() + .expect("VftManager::seed() should be called") + } + .message_info + .clone() + .into_iter() + .collect() +} + +/// Get mutable reference to a global message tracker. +pub fn msg_tracker_mut() -> &'static mut MessageTracker { + unsafe { + MSG_TRACKER + .as_mut() + .expect("VftManager::seed() should be called") + } +} + +impl MessageTracker { + /// Start tracking state of the message. + pub fn insert_message_info( + &mut self, + msg_id: MessageId, + status: MessageStatus, + details: TxDetails, + ) { + self.message_info + .insert(msg_id, MessageInfo { status, details }); + } + + /// Drive state machine further for a given `msg_id`. + pub fn update_message_status(&mut self, msg_id: MessageId, status: MessageStatus) { + if let Some(info) = self.message_info.get_mut(&msg_id) { + info.status = status; + } + } + + /// Get current state of the tracked message. Will return `None` if message isn't found. + pub fn get_message_info(&self, msg_id: &MessageId) -> Option<&MessageInfo> { + self.message_info.get(msg_id) + } + + /// Stop tracking message state. It will return current state of the target message. + pub fn remove_message_info(&mut self, msg_id: &MessageId) -> Option { + self.message_info.remove(msg_id) + } +} diff --git a/gear-programs/vft-manager/app/src/services/submit_receipt/token_operations.rs b/gear-programs/vft-manager/app/src/services/submit_receipt/token_operations.rs new file mode 100644 index 00000000..8aa2e0da --- /dev/null +++ b/gear-programs/vft-manager/app/src/services/submit_receipt/token_operations.rs @@ -0,0 +1,123 @@ +use gstd::msg; +use sails_rs::{calls::ActionIo, prelude::*}; + +use extended_vft_client::vft::io as vft_io; + +use super::{ + super::{Config, Error, TokenSupply}, + msg_tracker::{msg_tracker_mut, MessageStatus}, +}; + +/// Mint `amount` tokens into the `receiver` address. +/// +/// It will send `Mint` call to the corresponding `VFT` program and +/// asyncronously wait for the reply. +pub async fn mint( + token_id: ActorId, + receiver: ActorId, + amount: U256, + config: &Config, + msg_id: MessageId, +) -> Result<(), Error> { + let bytes: Vec = vft_io::Mint::encode_call(receiver, amount); + send_message_with_gas_for_reply( + token_id, + bytes, + config.gas_for_token_ops, + config.gas_for_reply_deposit, + config.reply_timeout, + msg_id, + ) + .await +} + +/// Transfer `amount` tokens from the current program address to the `receiver` address, +/// effectively unlocking them. +/// +/// It will send `TransferFrom` call to the corresponding `VFT` program and +/// asyncronously wait for the reply. +pub async fn unlock( + vara_token_id: ActorId, + recepient: ActorId, + amount: U256, + config: &Config, + msg_id: MessageId, +) -> Result<(), Error> { + let sender = gstd::exec::program_id(); + let bytes: Vec = vft_io::TransferFrom::encode_call(sender, recepient, amount); + + send_message_with_gas_for_reply( + vara_token_id, + bytes, + config.gas_for_token_ops, + config.gas_for_reply_deposit, + config.reply_timeout, + msg_id, + ) + .await +} + +/// Configure parameters for message sending and send message +/// asyncronously waiting for the reply. +/// +/// It will set reply hook to the [handle_reply_hook] and +/// timeout to the `reply_timeout`. +pub async fn send_message_with_gas_for_reply( + destination: ActorId, + message: Vec, + gas_to_send: u64, + gas_deposit: u64, + reply_timeout: u32, + msg_id: MessageId, +) -> Result<(), Error> { + gstd::msg::send_bytes_with_gas_for_reply(destination, message, gas_to_send, 0, gas_deposit) + .map_err(|_| Error::SendFailure)? + .up_to(Some(reply_timeout)) + .map_err(|_| Error::ReplyTimeout)? + .handle_reply(move || handle_reply_hook(msg_id)) + .map_err(|_| Error::ReplyHook)? + .await + .map_err(|_| Error::ReplyFailure)?; + + if let Some(info) = msg_tracker_mut().get_message_info(&msg_id) { + match info.status { + MessageStatus::TokenWithdrawComplete(true) => Ok(()), + MessageStatus::TokenWithdrawComplete(false) => Err(Error::MessageFailed), + _ => Err(Error::InvalidMessageStatus), + } + } else { + Err(Error::MessageNotFound) + } +} + +/// Handle reply received from `VFT` program. +/// +/// It will drive [MessageTracker](super::msg_tracker::MessageTracker) state machine further. +fn handle_reply_hook(msg_id: MessageId) { + let msg_tracker = msg_tracker_mut(); + + let msg_info = msg_tracker + .get_message_info(&msg_id) + .expect("Unexpected: msg info does not exist"); + let reply_bytes = msg::load_bytes().expect("Unable to load bytes"); + + if msg_info.status == MessageStatus::SendingMessageToWithdrawTokens { + let reply = match msg_info.details.token_supply { + TokenSupply::Ethereum => decode_mint_reply(&reply_bytes), + TokenSupply::Gear => decode_unlock_reply(&reply_bytes), + } + .unwrap_or(false); + + msg_tracker.update_message_status(msg_id, MessageStatus::TokenWithdrawComplete(reply)); + } +} + +/// Decode reply received from the `extended-vft::Mint` method. +fn decode_mint_reply(bytes: &[u8]) -> Result { + vft_io::Mint::decode_reply(bytes).map_err(|_| Error::MintTokensDecode) +} + +/// Decode reply received from the `extended-vft::TransferFrom` method. +fn decode_unlock_reply(bytes: &[u8]) -> Result { + vft_io::TransferFrom::decode_reply(bytes).map_err(|_| Error::TransferFromDecode) +} diff --git a/gear-programs/vft-manager/app/src/services/token_mapping.rs b/gear-programs/vft-manager/app/src/services/token_mapping.rs index f64c026e..fcf77c42 100644 --- a/gear-programs/vft-manager/app/src/services/token_mapping.rs +++ b/gear-programs/vft-manager/app/src/services/token_mapping.rs @@ -3,14 +3,21 @@ use sails_rs::prelude::*; use super::{error::Error, TokenSupply}; +/// Mapping between `VFT` and `ERC20` tokens. #[derive(Debug, Default)] pub struct TokenMap { + /// Mapping from `VFT` token addresses to `ERC20` token addresses. vara_to_eth: HashMap, + /// Mapping from `ERC20` token addresses to `VFT` token addresses. eth_to_vara: HashMap, + /// Mapping from `VFT` token addresses to the [TokenSupply] types. supply_mapping: HashMap, } impl TokenMap { + /// Insert token pair into the map. + /// + /// Will return error if either `vara_token_id` or `eth_token_id` is already present in the map. pub fn insert(&mut self, vara_token_id: ActorId, eth_token_id: H160, supply: TokenSupply) { if self .vara_to_eth @@ -33,6 +40,9 @@ impl TokenMap { } } + /// Remove token pair from map. + /// + /// Will return error if `vara_token_id` don't correspond to the already existing mapping. pub fn remove(&mut self, vara_token_id: ActorId) -> H160 { let eth_token_id = self .vara_to_eth @@ -52,6 +62,9 @@ impl TokenMap { eth_token_id } + /// Get `ERC20` token address by `VFT` token address. + /// + /// Will return error if mapping isn't found. pub fn get_eth_token_id(&self, vara_token_id: &ActorId) -> Result { self.vara_to_eth .get(vara_token_id) @@ -59,6 +72,9 @@ impl TokenMap { .ok_or(Error::NoCorrespondingEthAddress) } + /// Get `VFT` token address by `ERC20` token address. + /// + /// Will return error if mapping isn't found. pub fn get_vara_token_id(&self, eth_token_id: &H160) -> Result { self.eth_to_vara .get(eth_token_id) @@ -66,6 +82,9 @@ impl TokenMap { .ok_or(Error::NoCorrespondingVaraAddress) } + /// Get token pair [TokenSupply] type by `VFT` token address. + /// + /// Will return error if mapping isn't found. pub fn get_supply_type(&self, vara_token_id: &ActorId) -> Result { self.supply_mapping .get(vara_token_id) @@ -73,6 +92,7 @@ impl TokenMap { .ok_or(Error::NoCorrespondingVaraAddress) } + /// Read state of the token mapping. Will return all entries present in the mapping. pub fn read_state(&self) -> Vec<(ActorId, H160, TokenSupply)> { self.vara_to_eth .clone() diff --git a/gear-programs/vft-manager/app/src/services/token_operations.rs b/gear-programs/vft-manager/app/src/services/token_operations.rs deleted file mode 100644 index f4916df9..00000000 --- a/gear-programs/vft-manager/app/src/services/token_operations.rs +++ /dev/null @@ -1,126 +0,0 @@ -use super::msg_tracker::TxDetails; -use super::{msg_tracker_mut, utils, Config, Error, MessageStatus}; -use extended_vft_client::vft::io as vft_io; - -use sails_rs::prelude::*; - -pub async fn burn( - vara_token_id: ActorId, - sender: ActorId, - receiver: H160, - amount: U256, - config: &Config, - msg_id: MessageId, -) -> Result<(), Error> { - let bytes: Vec = vft_io::Burn::encode_call(sender, amount); - - let transaction_details = TxDetails::RequestBridging { - vara_token_id, - sender, - amount, - receiver, - }; - - msg_tracker_mut().insert_message_info( - msg_id, - MessageStatus::SendingMessageToBurnTokens, - transaction_details, - ); - - utils::set_critical_hook(msg_id); - utils::send_message_with_gas_for_reply( - vara_token_id, - bytes, - config.gas_for_token_ops, - config.gas_for_reply_deposit, - config.reply_timeout, - msg_id, - ) - .await?; - msg_tracker_mut().check_burn_result(&msg_id) -} - -pub async fn mint( - token_id: ActorId, - receiver: ActorId, - amount: U256, - config: &Config, - msg_id: MessageId, -) -> Result<(), Error> { - msg_tracker_mut().update_message_status(msg_id, MessageStatus::SendingMessageToMintTokens); - - let bytes: Vec = vft_io::Mint::encode_call(receiver, amount); - utils::send_message_with_gas_for_reply( - token_id, - bytes, - config.gas_for_token_ops, - config.gas_for_reply_deposit, - config.reply_timeout, - msg_id, - ) - .await?; - - msg_tracker_mut().check_mint_result(&msg_id) -} - -pub async fn lock( - vara_token_id: ActorId, - sender: ActorId, - amount: U256, - eth_receiver: H160, - config: &Config, - msg_id: MessageId, -) -> Result<(), Error> { - let receiver = gstd::exec::program_id(); - let bytes: Vec = vft_io::TransferFrom::encode_call(sender, receiver, amount); - - let transaction_details = TxDetails::RequestBridging { - vara_token_id, - sender, - amount, - receiver: eth_receiver, - }; - - msg_tracker_mut().insert_message_info( - msg_id, - MessageStatus::SendingMessageToLockTokens, - transaction_details, - ); - - utils::set_critical_hook(msg_id); - utils::send_message_with_gas_for_reply( - vara_token_id, - bytes, - config.gas_for_token_ops, - config.gas_for_reply_deposit, - config.reply_timeout, - msg_id, - ) - .await?; - - msg_tracker_mut().check_lock_result(&msg_id) -} - -pub async fn unlock( - vara_token_id: ActorId, - recepient: ActorId, - amount: U256, - config: &Config, - msg_id: MessageId, -) -> Result<(), Error> { - msg_tracker_mut().update_message_status(msg_id, MessageStatus::SendingMessageToUnlockTokens); - - let sender = gstd::exec::program_id(); - let bytes: Vec = vft_io::TransferFrom::encode_call(sender, recepient, amount); - utils::send_message_with_gas_for_reply( - vara_token_id, - bytes, - config.gas_for_token_ops, - config.gas_for_reply_deposit, - config.reply_timeout, - msg_id, - ) - .await?; - - msg_tracker_mut().check_unlock_result(&msg_id) -} diff --git a/gear-programs/vft-manager/app/src/services/utils.rs b/gear-programs/vft-manager/app/src/services/utils.rs deleted file mode 100644 index 5f4b6db5..00000000 --- a/gear-programs/vft-manager/app/src/services/utils.rs +++ /dev/null @@ -1,199 +0,0 @@ -use super::{error::Error, msg_tracker_mut, MessageStatus}; -use extended_vft_client::vft::io as vft_io; -use gstd::{msg, MessageId}; -use sails_rs::calls::ActionIo; -use sails_rs::prelude::*; - -pub fn set_critical_hook(msg_id: MessageId) { - gstd::critical::set_hook(move || { - let msg_tracker = msg_tracker_mut(); - let msg_info = msg_tracker - .get_message_info(&msg_id) - .expect("Unexpected: msg info does not exist"); - - match msg_info.status { - MessageStatus::SendingMessageToBurnTokens => { - // If still sending, transition to `WaitingReplyFromBurn`. - msg_tracker.update_message_status(msg_id, MessageStatus::WaitingReplyFromBurn); - } - MessageStatus::TokenBurnCompleted(true) => { - // If the token transfer is successful, continue to bridge builtin step. - msg_tracker.update_message_status(msg_id, MessageStatus::BridgeBuiltinStep); - } - MessageStatus::TokenBurnCompleted(false) => { - // If the token burn fails, cancel the transaction. - msg_tracker.remove_message_info(&msg_id); - } - - MessageStatus::SendingMessageToLockTokens => { - // If still sending, transition to `WaitingReplyFromLock`. - msg_tracker.update_message_status(msg_id, MessageStatus::WaitingReplyFromLock); - } - MessageStatus::TokenLockCompleted(true) => { - // If the token transfer is successful, continue to bridge builtin step. - msg_tracker.update_message_status(msg_id, MessageStatus::BridgeBuiltinStep); - } - MessageStatus::TokenLockCompleted(false) => { - // If the token lock fails, cancel the transaction. - msg_tracker.remove_message_info(&msg_id); - } - - MessageStatus::SendingMessageToBridgeBuiltin => { - // If still sending, transition to `WaitingReplyFromBuiltin`. - msg_tracker.update_message_status(msg_id, MessageStatus::WaitingReplyFromBuiltin); - } - MessageStatus::BridgeResponseReceived(None) => { - // If error occurs during builtin message, go to mint step - msg_tracker.update_message_status(msg_id, MessageStatus::MintTokensStep) - } - - MessageStatus::SendingMessageToMintTokens => { - msg_tracker.update_message_status(msg_id, MessageStatus::WaitingReplyFromMint); - } - - MessageStatus::SendingMessageToUnlockTokens => { - msg_tracker.update_message_status(msg_id, MessageStatus::WaitingReplyFromUnlock); - } - - _ => {} - }; - }); -} - -pub async fn send_message_with_gas_for_reply( - destination: ActorId, - message: Vec, - gas_to_send: u64, - gas_deposit: u64, - reply_timeout: u32, - msg_id: MessageId, -) -> Result<(), Error> { - gstd::msg::send_bytes_with_gas_for_reply(destination, message, gas_to_send, 0, gas_deposit) - .map_err(|_| Error::SendFailure)? - .up_to(Some(reply_timeout)) - .map_err(|_| Error::ReplyTimeout)? - .handle_reply(move || handle_reply_hook(msg_id)) - .map_err(|_| Error::ReplyHook)? - .await - .map_err(|_| Error::ReplyFailure)?; - Ok(()) -} - -fn handle_reply_hook(msg_id: MessageId) { - let msg_tracker = msg_tracker_mut(); - - let msg_info = msg_tracker - .get_message_info(&msg_id) - .expect("Unexpected: msg info does not exist"); - let reply_bytes = msg::load_bytes().expect("Unable to load bytes"); - - match msg_info.status { - MessageStatus::SendingMessageToBurnTokens => { - match decode_burn_reply(&reply_bytes) { - Ok(reply) => { - msg_tracker - .update_message_status(msg_id, MessageStatus::TokenBurnCompleted(reply)); - } - Err(_) => { - msg_tracker.remove_message_info(&msg_id); - } - }; - } - MessageStatus::WaitingReplyFromBurn => { - let reply = decode_burn_reply(&reply_bytes).unwrap_or(false); - if reply { - msg_tracker.update_message_status(msg_id, MessageStatus::BridgeBuiltinStep); - } else { - msg_tracker.remove_message_info(&msg_id); - } - } - - MessageStatus::SendingMessageToLockTokens => { - match decode_lock_reply(&reply_bytes) { - Ok(reply) => { - msg_tracker - .update_message_status(msg_id, MessageStatus::TokenLockCompleted(reply)); - } - Err(_) => { - msg_tracker.remove_message_info(&msg_id); - } - }; - } - MessageStatus::WaitingReplyFromLock => { - let reply = decode_lock_reply(&reply_bytes).unwrap_or(false); - if reply { - msg_tracker.update_message_status(msg_id, MessageStatus::BridgeBuiltinStep); - } else { - msg_tracker.remove_message_info(&msg_id); - } - } - - MessageStatus::SendingMessageToBridgeBuiltin => { - let reply = decode_bridge_reply(&reply_bytes); - let result = match reply { - Ok(Some(nonce)) => Some(nonce), - _ => None, - }; - msg_tracker - .update_message_status(msg_id, MessageStatus::BridgeResponseReceived(result)); - } - MessageStatus::WaitingReplyFromBuiltin => { - let reply = decode_bridge_reply(&reply_bytes); - match reply { - Ok(Some(nonce)) => { - msg_tracker.update_message_status( - msg_id, - MessageStatus::MessageProcessedWithSuccess(nonce), - ); - } - _ => { - msg_tracker.update_message_status(msg_id, MessageStatus::MintTokensStep); - } - }; - } - - MessageStatus::WaitingReplyFromMint | MessageStatus::SendingMessageToMintTokens => { - let reply = decode_mint_reply(&reply_bytes).unwrap_or(false); - if !reply { - msg_tracker.update_message_status(msg_id, MessageStatus::MintTokensStep); - } else { - msg_tracker.update_message_status(msg_id, MessageStatus::TokenMintCompleted); - } - } - - MessageStatus::WaitingReplyFromUnlock | MessageStatus::SendingMessageToUnlockTokens => { - let reply = decode_unlock_reply(&reply_bytes).unwrap_or(false); - if !reply { - msg_tracker.update_message_status(msg_id, MessageStatus::UnlockTokensStep); - } else { - msg_tracker.update_message_status(msg_id, MessageStatus::TokenUnlockCompleted); - } - } - _ => {} - }; -} - -fn decode_burn_reply(bytes: &[u8]) -> Result { - vft_io::Burn::decode_reply(bytes).map_err(|_| Error::BurnTokensDecode) -} - -fn decode_lock_reply(bytes: &[u8]) -> Result { - vft_io::TransferFrom::decode_reply(bytes).map_err(|_| Error::TransferFromDecode) -} - -fn decode_bridge_reply(mut bytes: &[u8]) -> Result, Error> { - let reply = - gbuiltin_eth_bridge::Response::decode(&mut bytes).map_err(|_| Error::BuiltinDecode)?; - - match reply { - gbuiltin_eth_bridge::Response::EthMessageQueued { nonce, .. } => Ok(Some(nonce)), - } -} - -fn decode_mint_reply(bytes: &[u8]) -> Result { - vft_io::Mint::decode_reply(bytes).map_err(|_| Error::MintTokensDecode) -} - -fn decode_unlock_reply(bytes: &[u8]) -> Result { - vft_io::TransferFrom::decode_reply(bytes).map_err(|_| Error::TransferFromDecode) -} diff --git a/gear-programs/vft-manager/tests/gtest.rs b/gear-programs/vft-manager/tests/gtest.rs index af29dc1d..e87a294c 100644 --- a/gear-programs/vft-manager/tests/gtest.rs +++ b/gear-programs/vft-manager/tests/gtest.rs @@ -2,7 +2,7 @@ use alloy_consensus::{Receipt, ReceiptEnvelope, ReceiptWithBloom}; use extended_vft_client::{traits::*, ExtendedVftFactory as VftFactoryC, Vft as VftC}; use gtest::{Program, System, WasmProgram}; use sails_rs::{calls::*, gtest::calls::*, prelude::*}; -use vft_manager_app::services::abi::ERC20_MANAGER; +use vft_manager_app::services::eth_abi::ERC20_MANAGER; use vft_manager_client::{ traits::*, Config, Error, InitConfig, TokenSupply, VftManager as VftManagerC, VftManagerFactory as VftManagerFactoryC, @@ -85,10 +85,8 @@ async fn setup_for_test() -> Fixture { config: Config { gas_for_token_ops: 15_000_000_000, gas_for_reply_deposit: 15_000_000_000, - gas_for_submit_receipt: 15_000_000_000, gas_to_send_request_to_builtin: 15_000_000_000, reply_timeout: 100, - gas_for_request_bridging: 20_000_000_000, }, }; let vft_manager_program_id = VftManagerFactoryC::new(remoting.clone()) @@ -300,7 +298,7 @@ async fn test_withdraw_fails_with_bad_origin() { .await .unwrap(); - assert_eq!(result.unwrap_err(), Error::NotEthClient); + assert_eq!(result.unwrap_err(), Error::NotHistoricalProxy); } async fn balance_of(