diff --git a/Cargo.lock b/Cargo.lock index dbd5e81f3..34cae1abe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,6 @@ dependencies = [ "base64 0.13.0", "blake2 0.9.1 (git+https://github.com/near/near-blake2.git)", "borsh 0.8.2", - "byte-slice-cast", "ethabi", "evm", "evm-core", diff --git a/engine-precompiles/Cargo.toml b/engine-precompiles/Cargo.toml index c73a4bd03..1bc39e484 100644 --- a/engine-precompiles/Cargo.toml +++ b/engine-precompiles/Cargo.toml @@ -32,7 +32,6 @@ wee_alloc = { version = "0.4.5", default-features = false } logos = { version = "0.12", default-features = false, features = ["export_derive"] } ethabi = { git = "https://github.com/darwinia-network/ethabi", branch = "xavier-no-std", default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } -byte-slice-cast = { version = "1.0", default-features = false } rjson = { git = "https://github.com/aurora-is-near/rjson", rev = "cc3da949", default-features = false, features = ["integer"] } [dev-dependencies] diff --git a/engine-standalone-storage/src/relayer_db/mod.rs b/engine-standalone-storage/src/relayer_db/mod.rs index 9cd82465e..5fcc3f191 100644 --- a/engine-standalone-storage/src/relayer_db/mod.rs +++ b/engine-standalone-storage/src/relayer_db/mod.rs @@ -218,7 +218,7 @@ mod test { .unwrap(); let mut io = storage.access_engine_storage_at_position(block_height, 0, &[]); engine::set_state(&mut io, engine_state.clone()); - connector::EthConnectorContract::init_contract( + connector::EthConnectorContract::create_contract( io, engine_state.owner_id.clone(), parameters::InitCallArgs { diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 8568598ec..6113d1b31 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -107,7 +107,7 @@ pub fn consume_message(storage: &mut crate::Storage, message: Message) -> Result engine::Engine::new(relayer_address, env.current_account_id(), io, &env)?; if env.predecessor_account_id == env.current_account_id { - connector::EthConnectorContract::get_instance(io) + connector::EthConnectorContract::init_instance(io) .ft_on_transfer(&engine, &args)?; } else { engine.receive_erc20_tokens( @@ -123,7 +123,7 @@ pub fn consume_message(storage: &mut crate::Storage, message: Message) -> Result } TransactionKind::Deposit(raw_proof) => { - let mut connector_contract = connector::EthConnectorContract::get_instance(io); + let mut connector_contract = connector::EthConnectorContract::init_instance(io); let promise_args = connector_contract.deposit( raw_proof, env.current_account_id(), diff --git a/engine-tests/Cargo.toml b/engine-tests/Cargo.toml index b3a2876a3..61b163812 100644 --- a/engine-tests/Cargo.toml +++ b/engine-tests/Cargo.toml @@ -20,7 +20,6 @@ aurora-engine-precompiles = { path = "../engine-precompiles", default-features = engine-standalone-storage = { path = "../engine-standalone-storage", default-features = false } engine-standalone-tracing = { path = "../engine-standalone-tracing", default-features = false } borsh = { version = "0.8.2", default-features = false } -byte-slice-cast = { version = "1.0", default-features = false } sha3 = { version = "0.9.1", default-features = false } evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } @@ -30,6 +29,7 @@ rlp = { version = "0.5.0", default-features = false } [dev-dependencies] base64 = "0.13.0" bstr = "0.2" +byte-slice-cast = { version = "1.0", default-features = false } ethabi = { git = "https://github.com/darwinia-network/ethabi", branch = "xavier-no-std" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/engine-tests/src/test_utils/standalone/mocks/mod.rs b/engine-tests/src/test_utils/standalone/mocks/mod.rs index 1dd191d79..4439ebba9 100644 --- a/engine-tests/src/test_utils/standalone/mocks/mod.rs +++ b/engine-tests/src/test_utils/standalone/mocks/mod.rs @@ -64,7 +64,7 @@ pub fn init_evm(mut io: I, env: &E) { metadata: FungibleTokenMetadata::default(), }; - aurora_engine::connector::EthConnectorContract::init_contract( + aurora_engine::connector::EthConnectorContract::create_contract( io, env.current_account_id(), connector_args, @@ -102,7 +102,7 @@ pub fn mint_evm_account( amount: balance.raw().low_u128(), proof_key: String::new(), relayer_id: aurora_account_id.clone(), - fee: 0, + fee: 0.into(), msg: None, }; @@ -113,7 +113,7 @@ pub fn mint_evm_account( ); io.remove_storage(&proof_key); - aurora_engine::connector::EthConnectorContract::get_instance(io) + aurora_engine::connector::EthConnectorContract::init_instance(io) .finish_deposit( aurora_account_id.clone(), aurora_account_id.clone(), diff --git a/engine-tests/src/tests/eth_connector.rs b/engine-tests/src/tests/eth_connector.rs index e055e7477..7f1de673c 100644 --- a/engine-tests/src/tests/eth_connector.rs +++ b/engine-tests/src/tests/eth_connector.rs @@ -1,6 +1,5 @@ use crate::prelude::EthAddress; use crate::prelude::WithdrawCallArgs; -use crate::prelude::U256; use crate::test_utils::str_to_account_id; use aurora_engine::admin_controlled::{PausedMask, ERR_PAUSED}; use aurora_engine::connector::{ @@ -10,8 +9,10 @@ use aurora_engine::fungible_token::FungibleTokenMetadata; use aurora_engine::parameters::{ InitCallArgs, NewCallArgs, RegisterRelayerCallArgs, WithdrawResult, }; +use aurora_engine_types::types::Fee; use borsh::{BorshDeserialize, BorshSerialize}; use byte_slice_cast::AsByteSlice; +use ethabi::ethereum_types::U256; use near_sdk::test_utils::accounts; use near_sdk_sim::transaction::ExecutionStatus; use near_sdk_sim::{to_yocto, ExecutionResult, UserAccount, DEFAULT_GAS, STORAGE_AMOUNT}; @@ -409,9 +410,10 @@ fn test_ft_transfer_call_eth() { res.assert_success(); let transfer_amount = 50; - let fee = 30; + let fee: u128 = 30; let mut msg = U256::from(fee).as_byte_slice().to_vec(); msg.append(&mut validate_eth_address(RECIPIENT_ETH_ADDRESS).to_vec()); + let message = [CONTRACT_ACC, hex::encode(msg).as_str()].join(":"); let res = contract.call( CONTRACT_ACC.parse().unwrap(), @@ -592,6 +594,7 @@ fn test_ft_transfer_call_without_message() { #[test] fn test_deposit_with_0x_prefix() { + use aurora_engine::deposit_event::TokenMessageData; let (master_account, contract) = init(CUSTODIAN_ADDRESS); let eth_custodian_address: [u8; 20] = { @@ -601,20 +604,22 @@ fn test_deposit_with_0x_prefix() { buf }; let recipient_address = [10u8; 20]; - let deposit_amount = U256::from(17); + let deposit_amount = 17; + let recipient_address_encoded = hex::encode(&recipient_address); + + // Note the 0x prefix before the deposit address. + let message = [CONTRACT_ACC, ":", "0x", &recipient_address_encoded].concat(); + let fee: Fee = 0.into(); + let token_message_data = + TokenMessageData::parse_event_message_and_prepare_token_message_data(&message, fee) + .unwrap(); + let deposit_event = aurora_engine::deposit_event::DepositedEvent { eth_custodian_address, sender: [0u8; 20], - // Note the 0x prefix before the deposit address. - recipient: [ - CONTRACT_ACC, - ":", - "0x", - hex::encode(&recipient_address).as_str(), - ] - .concat(), + token_message_data, amount: deposit_amount, - fee: U256::zero(), + fee, }; let event_schema = ethabi::Event { @@ -630,9 +635,9 @@ fn test_deposit_with_0x_prefix() { crate::prelude::H256::zero(), ], data: ethabi::encode(&[ - ethabi::Token::String(deposit_event.recipient), - ethabi::Token::Uint(deposit_event.amount), - ethabi::Token::Uint(deposit_event.fee), + ethabi::Token::String(message), + ethabi::Token::Uint(U256::from(deposit_event.amount)), + ethabi::Token::Uint(U256::from(deposit_event.fee.into_u128())), ]), }; let proof = Proof { @@ -655,9 +660,9 @@ fn test_deposit_with_0x_prefix() { res.assert_success(); let aurora_balance = get_eth_on_near_balance(&master_account, CONTRACT_ACC, CONTRACT_ACC); - assert_eq!(aurora_balance, deposit_amount.low_u128()); + assert_eq!(aurora_balance, deposit_amount); let address_balance = get_eth_balance(&master_account, recipient_address, CONTRACT_ACC); - assert_eq!(address_balance, deposit_amount.low_u128()); + assert_eq!(address_balance, deposit_amount); } #[test] @@ -711,7 +716,7 @@ fn test_ft_transfer_call_without_relayer() { assert_eq!(balance, DEPOSITED_FEE); let transfer_amount = 50; - let fee = 30; + let fee: u128 = 30; let mut msg = U256::from(fee).as_byte_slice().to_vec(); msg.append(&mut validate_eth_address(RECIPIENT_ETH_ADDRESS).to_vec()); let relayer_id = "relayer.root"; @@ -767,8 +772,8 @@ fn test_ft_transfer_call_fee_greater_than_amount() { call_deposit_eth_to_near(&contract, CONTRACT_ACC); let transfer_amount = 10; - let fee = transfer_amount + 10; - let mut msg = U256::from(fee).as_byte_slice().to_vec(); + let fee: u128 = transfer_amount + 10; + let mut msg = fee.to_be_bytes().to_vec(); msg.append(&mut validate_eth_address(RECIPIENT_ETH_ADDRESS).to_vec()); let relayer_id = "relayer.root"; let message = [relayer_id, hex::encode(msg).as_str()].join(":"); diff --git a/engine-tests/src/tests/standalone/sync.rs b/engine-tests/src/tests/standalone/sync.rs index 86c094fc6..00c6de10f 100644 --- a/engine-tests/src/tests/standalone/sync.rs +++ b/engine-tests/src/tests/standalone/sync.rs @@ -1,4 +1,6 @@ +use aurora_engine::deposit_event::TokenMessageData; use aurora_engine_sdk::env::{Env, Timestamp}; +use aurora_engine_types::types::Fee; use aurora_engine_types::{account_id::AccountId, types::Wei, Address, H256, U256}; use borsh::BorshSerialize; use engine_standalone_storage::sync; @@ -331,12 +333,19 @@ fn test_consume_submit_message() { fn mock_proof(recipient_address: Address, deposit_amount: Wei) -> aurora_engine::proof::Proof { let eth_custodian_address = test_utils::standalone::mocks::ETH_CUSTODIAN_ADDRESS; + + let fee = Fee::new(0); + let message = ["aurora", ":", hex::encode(&recipient_address).as_str()].concat(); + let token_message_data: TokenMessageData = + TokenMessageData::parse_event_message_and_prepare_token_message_data(&message, fee) + .unwrap(); + let deposit_event = aurora_engine::deposit_event::DepositedEvent { eth_custodian_address: eth_custodian_address.0, sender: [0u8; 20], - recipient: ["aurora", ":", hex::encode(&recipient_address).as_str()].concat(), - amount: deposit_amount.raw(), - fee: U256::zero(), + token_message_data, + amount: deposit_amount.raw().as_u128(), + fee, }; let event_schema = ethabi::Event { @@ -352,9 +361,9 @@ fn mock_proof(recipient_address: Address, deposit_amount: Wei) -> aurora_engine: crate::prelude::H256::zero(), ], data: ethabi::encode(&[ - ethabi::Token::String(deposit_event.recipient), - ethabi::Token::Uint(deposit_event.amount), - ethabi::Token::Uint(deposit_event.fee), + ethabi::Token::String(message), + ethabi::Token::Uint(U256::from(deposit_event.amount)), + ethabi::Token::Uint(U256::from(deposit_event.fee.into_u128())), ]), }; aurora_engine::proof::Proof { diff --git a/engine-types/src/types.rs b/engine-types/src/types.rs index 6001a9d44..90ab6699d 100644 --- a/engine-types/src/types.rs +++ b/engine-types/src/types.rs @@ -3,10 +3,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::fmt::Formatter; +// TODO: introduce new Balance type for more strict typing pub type Balance = u128; pub type RawAddress = [u8; 20]; -pub type RawU256 = [u8; 32]; // Big-endian large integer type. +pub type RawU256 = [u8; 32]; +// Big-endian large integer type. pub type RawH256 = [u8; 32]; // Unformatted binary data of fixed length. + +// TODO: introduce new type. Add encode/decode/validation methods pub type EthAddress = [u8; 20]; pub type StorageUsage = u64; /// Wei compatible Borsh-encoded raw value to attach an ETH balance to the transaction @@ -104,6 +108,44 @@ impl Mul for u64 { } } +#[derive( + Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, BorshSerialize, BorshDeserialize, +)] +/// Engine `fee` type which wraps an underlying u128. +pub struct Fee(u128); + +impl Display for Fee { + fn fmt(&self, f: &mut Formatter<'_>) -> crate::fmt::Result { + self.0.fmt(f) + } +} + +impl Fee { + /// Constructs a new `Fee` with a given u128 value. + pub const fn new(fee: u128) -> Fee { + Self(fee) + } + + /// Consumes `Fee` and returns the underlying type. + pub fn into_u128(self) -> u128 { + self.0 + } +} + +impl Add for Fee { + type Output = Fee; + + fn add(self, rhs: Fee) -> Self::Output { + Fee(self.0 + rhs.0) + } +} + +impl From for Fee { + fn from(fee: u128) -> Self { + Self(fee) + } +} + /// Selector to call mint function in ERC 20 contract /// /// keccak("mint(address,uint256)".as_bytes())[..4]; @@ -125,7 +167,7 @@ impl AsRef<[u8]> for AddressValidationError { } } -/// Validate Etherium address from string and return EthAddress +/// Validate Ethereum address from string and return Result data EthAddress or Error data pub fn validate_eth_address(address: String) -> Result { let data = hex::decode(address).map_err(|_| AddressValidationError::FailedDecodeHex)?; if data.len() != 20 { @@ -183,6 +225,13 @@ impl Wei { pub fn checked_add(self, rhs: Self) -> Option { self.0.checked_add(rhs.0).map(Self) } + + /// Try convert U256 to u128 with checking overflow. + /// NOTICE: Error can contain only overflow + pub fn try_into_u128(self) -> Result { + use crate::TryInto; + self.0.try_into().map_err(|_| error::BalanceOverflowError) + } } impl Display for Wei { @@ -214,10 +263,8 @@ impl From for Wei { } } -#[derive(BorshSerialize, BorshDeserialize)] -pub struct U128(pub u128); - -pub const STORAGE_PRICE_PER_BYTE: u128 = 10_000_000_000_000_000_000; // 1e19yN, 0.00001N +pub const STORAGE_PRICE_PER_BYTE: u128 = 10_000_000_000_000_000_000; +// 1e19yN, 0.00001N pub const ERR_FAILED_PARSE: &str = "ERR_FAILED_PARSE"; pub const ERR_INVALID_ETH_ADDRESS: &str = "ERR_INVALID_ETH_ADDRESS"; @@ -314,10 +361,31 @@ impl Stack { self.stack } } + pub fn str_from_slice(inp: &[u8]) -> &str { str::from_utf8(inp).unwrap() } +pub mod error { + use crate::{fmt, String}; + + #[derive(Eq, Hash, Clone, Debug, PartialEq)] + pub struct BalanceOverflowError; + + impl AsRef<[u8]> for BalanceOverflowError { + fn as_ref(&self) -> &[u8] { + b"ERR_BALANCE_OVERFLOW" + } + } + + impl fmt::Display for BalanceOverflowError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = String::from_utf8(self.as_ref().to_vec()).unwrap(); + write!(f, "{}", msg) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -420,4 +488,20 @@ mod tests { let wei_amount = U256::from(eth_amount) * U256::from(10).pow(18.into()); assert_eq!(Wei::from_eth(eth_amount.into()), Some(Wei::new(wei_amount))); } + + #[test] + fn test_fee_add() { + let fee = Fee::new(100); + assert_eq!(fee + fee, Fee::new(200)); + assert_eq!(fee.add(200.into()), Fee::new(300)); + } + + #[test] + fn test_fee_from() { + let fee = Fee::new(100); + let fee2 = Fee::from(100u128); + assert_eq!(fee, fee2); + let res: u128 = fee.into_u128(); + assert_eq!(res, 100); + } } diff --git a/engine/src/connector.rs b/engine/src/connector.rs index 76d435b81..ff9a1e74b 100644 --- a/engine/src/connector.rs +++ b/engine/src/connector.rs @@ -1,5 +1,5 @@ use crate::admin_controlled::{AdminControlled, PausedMask}; -use crate::deposit_event::DepositedEvent; +use crate::deposit_event::{DepositedEvent, FtTransferMessageData, TokenMessageData}; use crate::engine::Engine; use crate::fungible_token::{self, FungibleToken, FungibleTokenMetadata, FungibleTokenOps}; use crate::parameters::{ @@ -10,27 +10,38 @@ use crate::parameters::{ }; use crate::prelude::{ format, sdk, str, validate_eth_address, AccountId, Address, Balance, BorshDeserialize, - BorshSerialize, EthAddress, EthConnectorStorageId, KeyPrefix, NearGas, PromiseResult, String, - ToString, TryFrom, Vec, WithdrawCallArgs, ERR_FAILED_PARSE, H160, U256, + BorshSerialize, EthAddress, EthConnectorStorageId, KeyPrefix, NearGas, PromiseResult, ToString, + Vec, WithdrawCallArgs, ERR_FAILED_PARSE, H160, +}; +use crate::prelude::{ + AddressValidationError, PromiseBatchAction, PromiseCreateArgs, PromiseWithCallbackArgs, }; use crate::proof::Proof; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::{StorageIntermediate, IO}; -use aurora_engine_types::parameters::{ - PromiseBatchAction, PromiseCreateArgs, PromiseWithCallbackArgs, -}; -use aurora_engine_types::types::AddressValidationError; pub const ERR_NOT_ENOUGH_BALANCE_FOR_FEE: &str = "ERR_NOT_ENOUGH_BALANCE_FOR_FEE"; -pub const NO_DEPOSIT: Balance = 0; +/// Indicate zero attached balance for promise call +pub const ZERO_ATTACHED_BALANCE: Balance = 0; +/// NEAR Gas for calling `fininsh_deposit` promise. Used in the `deposit` logic. const GAS_FOR_FINISH_DEPOSIT: NearGas = NearGas::new(50_000_000_000_000); +/// NEAR Gas for calling `verify_log_entry` promise. Used in the `deposit` logic. // Note: Is 40Tgas always enough? const GAS_FOR_VERIFY_LOG_ENTRY: NearGas = NearGas::new(40_000_000_000_000); +/// Admin control flow flag indicates that all control flow unpause (unblocked). pub const UNPAUSE_ALL: PausedMask = 0; +/// Admin control flow flag indicates that the deposit is paused. pub const PAUSE_DEPOSIT: PausedMask = 1 << 0; +/// Admin control flow flag indicates that withdrawal is paused. pub const PAUSE_WITHDRAW: PausedMask = 1 << 1; +/// Eth-connector contract data. It's stored in the storage. +/// Contains: +/// * connector specific data +/// * Fungible token data +/// * paused_mask - admin control flow data +/// * io - I/O trait handler pub struct EthConnectorContract { contract: EthConnector, ft: FungibleTokenOps, @@ -38,33 +49,20 @@ pub struct EthConnectorContract { io: I, } -/// eth-connector specific data +/// Connector specific data. It always should contain `prover account` - #[derive(BorshSerialize, BorshDeserialize)] pub struct EthConnector { + /// It used in the Deposit flow, to verify log entry form incoming proof. pub prover_account: AccountId, + /// It is Eth address, used in the Deposit and Withdraw logic. pub eth_custodian_address: EthAddress, } -/// Token message data -#[derive(BorshSerialize, BorshDeserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] -pub enum TokenMessageData { - Near(AccountId), - Eth { - receiver_id: AccountId, - message: String, - }, -} - -/// On-transfer message -pub struct OnTransferMessageData { - pub relayer: AccountId, - pub recipient: EthAddress, - pub fee: U256, -} - impl EthConnectorContract { - pub fn get_instance(io: I) -> Self { + /// Init Eth-connector contract instance. + /// Load contract data from storage and init I/O handler. + /// Used as single point of contract access for various contract actions + pub fn init_instance(io: I) -> Self { Self { contract: get_contract_data(&io, &EthConnectorStorageId::Contract), ft: get_contract_data::(&io, &EthConnectorStorageId::FungibleToken) @@ -74,8 +72,10 @@ impl EthConnectorContract { } } - /// Init eth-connector contract specific data - pub fn init_contract( + /// Create contract data - init eth-connector contract specific data. + /// Used only once for first time initialization. + /// Initialized contract data stored in the storage. + pub fn create_contract( mut io: I, owner_id: AccountId, args: InitCallArgs, @@ -120,80 +120,6 @@ impl EthConnectorContract { Ok(()) } - /// Parse event message data for tokens - fn parse_event_message( - &self, - message: &str, - ) -> Result { - let data: Vec<_> = message.split(':').collect(); - if data.len() >= 3 { - return Err(error::ParseEventMessageError::TooManyParts); - } - let account_id = AccountId::try_from(data[0].as_bytes()) - .map_err(|_| error::ParseEventMessageError::InvalidAccount)?; - if data.len() == 1 { - Ok(TokenMessageData::Near(account_id)) - } else { - Ok(TokenMessageData::Eth { - receiver_id: account_id, - message: data[1].into(), - }) - } - } - - /// Get on-transfer data from message - fn parse_on_transfer_message( - &self, - message: &str, - ) -> Result { - let data: Vec<_> = message.split(':').collect(); - if data.len() != 2 { - return Err(error::ParseOnTransferMessageError::TooManyParts); - } - - let msg = - hex::decode(data[1]).map_err(|_| error::ParseOnTransferMessageError::InvalidHexData)?; - let mut fee: [u8; 32] = Default::default(); - if msg.len() != 52 { - return Err(error::ParseOnTransferMessageError::WrongMessageFormat); - } - fee.copy_from_slice(&msg[..32]); - let mut recipient: EthAddress = Default::default(); - recipient.copy_from_slice(&msg[32..52]); - // Check account - let account_id = AccountId::try_from(data[0].as_bytes()) - .map_err(|_| error::ParseOnTransferMessageError::InvalidAccount)?; - Ok(OnTransferMessageData { - relayer: account_id, - recipient, - fee: U256::from_little_endian(&fee[..]), - }) - } - - /// Prepare message for `ft_transfer_call` -> `ft_on_transfer` - fn prepare_message_for_on_transfer( - &self, - relayer_account_id: &AccountId, - fee: U256, - message: String, - ) -> Result { - use byte_slice_cast::AsByteSlice; - - let mut data = fee.as_byte_slice().to_vec(); - let address = if message.len() == 42 { - message - .strip_prefix("0x") - .ok_or(AddressValidationError::FailedDecodeHex)? - .to_string() - } else { - message - }; - let address_bytes = - hex::decode(address).map_err(|_| AddressValidationError::FailedDecodeHex)?; - data.extend(address_bytes); - Ok([relayer_account_id.as_ref(), &hex::encode(data)].join(":")) - } - /// Deposit all types of tokens pub fn deposit( &self, @@ -201,7 +127,9 @@ impl EthConnectorContract { current_account_id: AccountId, predecessor_account_id: AccountId, ) -> Result { + // Check is current account owner let is_owner = current_account_id == predecessor_account_id; + // Check is current flow paused. If it's owner account just skip it. self.assert_not_paused(PAUSE_DEPOSIT, is_owner) .map_err(|_| error::DepositError::Paused)?; @@ -217,9 +145,9 @@ impl EthConnectorContract { sdk::log!(&format!( "Deposit started: from {} to recipient {:?} with amount: {:?} and fee {:?}", hex::encode(event.sender), - event.recipient, - event.amount.as_u128(), - event.fee.as_u128() + event.token_message_data.get_recipient(), + event.amount, + event.fee )); sdk::log!(&format!( @@ -232,7 +160,7 @@ impl EthConnectorContract { return Err(error::DepositError::CustodianAddressMismatch); } - if event.fee >= event.amount { + if event.fee.into_u128() >= event.amount { return Err(error::DepositError::InsufficientAmountForFee); } @@ -251,19 +179,19 @@ impl EthConnectorContract { target_account_id: self.contract.prover_account.clone(), method: "verify_log_entry".to_string(), args: proof_to_verify, - attached_balance: NO_DEPOSIT, + attached_balance: ZERO_ATTACHED_BALANCE, attached_gas: GAS_FOR_VERIFY_LOG_ENTRY.into_u64(), }; // Finalize deposit - let data = match self.parse_event_message(&event.recipient)? { + let data = match event.token_message_data { // Deposit to NEAR accounts TokenMessageData::Near(account_id) => FinishDepositCallArgs { new_owner_id: account_id, - amount: event.amount.as_u128(), + amount: event.amount, proof_key: proof.get_key(), relayer_id: predecessor_account_id, - fee: event.fee.as_u128(), + fee: event.fee, msg: None, } .try_to_vec() @@ -278,15 +206,9 @@ impl EthConnectorContract { // address - is NEAR account let transfer_data = TransferCallCallArgs { receiver_id, - amount: event.amount.as_u128(), + amount: event.amount, memo: None, - msg: self - .prepare_message_for_on_transfer( - &predecessor_account_id, - event.fee, - message, - ) - .map_err(error::DepositError::InvalidAddress)?, + msg: message.encode(), } .try_to_vec() .unwrap(); @@ -294,10 +216,10 @@ impl EthConnectorContract { // Send to self - current account id FinishDepositCallArgs { new_owner_id: current_account_id.clone(), - amount: event.amount.as_u128(), + amount: event.amount, proof_key: proof.get_key(), relayer_id: predecessor_account_id, - fee: event.fee.as_u128(), + fee: event.fee, msg: Some(transfer_data), } .try_to_vec() @@ -309,7 +231,7 @@ impl EthConnectorContract { target_account_id: current_account_id, method: "finish_deposit".to_string(), args: data, - attached_balance: NO_DEPOSIT, + attached_balance: ZERO_ATTACHED_BALANCE, attached_gas: GAS_FOR_FINISH_DEPOSIT.into_u64(), }; Ok(PromiseWithCallbackArgs { @@ -348,8 +270,11 @@ impl EthConnectorContract { Ok(Some(promise)) } else { // Mint - calculate new balances - self.mint_eth_on_near(data.new_owner_id.clone(), data.amount - data.fee)?; - self.mint_eth_on_near(data.relayer_id, data.fee)?; + self.mint_eth_on_near( + data.new_owner_id.clone(), + data.amount - data.fee.into_u128(), + )?; + self.mint_eth_on_near(data.relayer_id, data.fee.into_u128())?; // Store proof only after `mint` calculations self.record_proof(&data.proof_key)?; // Save new contract data @@ -362,9 +287,9 @@ impl EthConnectorContract { pub(crate) fn internal_remove_eth( &mut self, address: &Address, - amount: &U256, + amount: Balance, ) -> Result<(), fungible_token::error::WithdrawError> { - self.burn_eth_on_aurora(address.0, amount.as_u128())?; + self.burn_eth_on_aurora(address.0, amount)?; self.save_ft_contract(); Ok(()) } @@ -373,7 +298,7 @@ impl EthConnectorContract { fn record_proof(&mut self, key: &str) -> Result<(), error::ProofUsed> { sdk::log!(&format!("Record proof: {}", key)); - if self.check_used_event(key) { + if self.is_used_event(key) { return Err(error::ProofUsed); } @@ -431,7 +356,9 @@ impl EthConnectorContract { predecessor_account_id: &AccountId, args: WithdrawCallArgs, ) -> Result { + // Check is current account id is owner let is_owner = current_account_id == predecessor_account_id; + // Check is current flow paused. If it's owner just skip asserrion. self.assert_not_paused(PAUSE_WITHDRAW, is_owner) .map_err(|_| error::WithdrawError::Paused)?; @@ -477,10 +404,13 @@ impl EthConnectorContract { } /// Return balance of ETH (ETH in Aurora EVM) - pub fn ft_balance_of_eth_on_aurora(&mut self, args: BalanceOfEthCallArgs) { + pub fn ft_balance_of_eth_on_aurora( + &mut self, + args: BalanceOfEthCallArgs, + ) -> Result<(), crate::prelude::types::error::BalanceOverflowError> { let balance = self .ft - .internal_unwrap_balance_of_eth_on_aurora(args.address); + .internal_unwrap_balance_of_eth_on_aurora(args.address)?; sdk::log!(&format!( "Balance of ETH [{}]: {}", hex::encode(args.address), @@ -488,6 +418,7 @@ impl EthConnectorContract { )); self.io .return_output(format!("\"{}\"", balance.to_string()).as_bytes()); + Ok(()) } /// Transfer between NEAR accounts @@ -535,6 +466,7 @@ impl EthConnectorContract { /// FT transfer call from sender account (invoker account) to receiver /// We starting early checking for message data to avoid `ft_on_transfer` call panics /// But we don't check relayer exists. If relayer doesn't exist we simply not mint/burn the amount of the fee + /// We allow empty messages for cases when `receiver_id =! current_account_id` pub fn ft_transfer_call( &mut self, predecessor_account_id: AccountId, @@ -545,11 +477,14 @@ impl EthConnectorContract { "Transfer call to {} amount {}", args.receiver_id, args.amount, )); + // Verify message data before `ft_on_transfer` call to avoid verification panics + // It's allowed empty message if `receiver_id =! current_account_id` if args.receiver_id == current_account_id { - let message_data = self.parse_on_transfer_message(&args.msg)?; + let message_data = FtTransferMessageData::parse_on_transfer_message(&args.msg) + .map_err(error::FtTransferCallError::MessageParseFailed)?; // Check is transfer amount > fee - if message_data.fee.as_u128() >= args.amount { + if message_data.fee.into_u128() >= args.amount { return Err(error::FtTransferCallError::InsufficientAmountForFee); } @@ -558,7 +493,8 @@ impl EthConnectorContract { // Note: It can't overflow because the total supply doesn't change during transfer. let amount_for_check = self .ft - .internal_unwrap_balance_of_eth_on_aurora(message_data.recipient); + .internal_unwrap_balance_of_eth_on_aurora(message_data.recipient) + .map_err(error::FtTransferCallError::BalanceOverflow)?; if amount_for_check.checked_add(args.amount).is_none() { return Err(error::FtTransferCallError::Transfer( fungible_token::error::TransferError::BalanceOverflow, @@ -655,10 +591,11 @@ impl EthConnectorContract { ) -> Result<(), error::FtTransferCallError> { sdk::log!("Call ft_on_transfer"); // Parse message with specific rules - let message_data = self.parse_on_transfer_message(&args.msg)?; + let message_data = FtTransferMessageData::parse_on_transfer_message(&args.msg) + .map_err(error::FtTransferCallError::MessageParseFailed)?; // Special case when predecessor_account_id is current_account_id - let fee = message_data.fee.as_u128(); + let fee = message_data.fee.into_u128(); // Mint fee to relayer let relayer = engine.get_relayer(message_data.relayer.as_bytes()); match (fee, relayer) { @@ -680,7 +617,7 @@ impl EthConnectorContract { .return_output(&self.ft.get_accounts_counter().to_le_bytes()); } - /// Save eth-connector contract data + /// Save eth-connector fungible token contract data fn save_ft_contract(&mut self) { self.io.write_borsh( &construct_contract_key(&EthConnectorStorageId::FungibleToken), @@ -688,7 +625,7 @@ impl EthConnectorContract { ); } - /// Generate key for used events from Prood + /// Generate key for used events from Proof fn used_event_key(&self, key: &str) -> Vec { let mut v = construct_contract_key(&EthConnectorStorageId::UsedEvent).to_vec(); v.extend_from_slice(key.as_bytes()); @@ -701,13 +638,13 @@ impl EthConnectorContract { } /// Check is event of proof already used - fn check_used_event(&self, key: &str) -> bool { + fn is_used_event(&self, key: &str) -> bool { self.io.storage_has_key(&self.used_event_key(key)) } /// Checks whether the provided proof was already used pub fn is_used_proof(&self, proof: Proof) -> bool { - self.check_used_event(&proof.get_key()) + self.is_used_event(&proof.get_key()) } /// Get Eth connector paused flags @@ -722,10 +659,12 @@ impl EthConnectorContract { } impl AdminControlled for EthConnectorContract { + /// Get current admin paused status fn get_paused(&self) -> PausedMask { self.paused_mask } + /// Set admin paused status fn set_paused(&mut self, paused_mask: PausedMask) { self.paused_mask = paused_mask; self.io.write_borsh( @@ -779,42 +718,23 @@ pub fn get_metadata(io: &I) -> Option { } pub mod error { - use aurora_engine_types::types::AddressValidationError; + use crate::prelude::types::{error::BalanceOverflowError, AddressValidationError}; + use crate::deposit_event::error::ParseOnTransferMessageError; use crate::{deposit_event, fungible_token}; const PROOF_EXIST: &[u8; 15] = b"ERR_PROOF_EXIST"; - const INVALID_ACCOUNT: &[u8; 22] = b"ERR_INVALID_ACCOUNT_ID"; - #[derive(Debug)] - pub enum ParseEventMessageError { - TooManyParts, - InvalidAccount, - } - impl AsRef<[u8]> for ParseEventMessageError { - fn as_ref(&self) -> &[u8] { - match self { - Self::TooManyParts => b"ERR_INVALID_EVENT_MESSAGE_FORMAT", - Self::InvalidAccount => INVALID_ACCOUNT, - } - } - } - - #[derive(Debug)] + #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] pub enum DepositError { Paused, ProofParseFailed, EventParseFailed(deposit_event::error::ParseError), CustodianAddressMismatch, InsufficientAmountForFee, - MessageParseFailed(ParseEventMessageError), InvalidAddress(AddressValidationError), } - impl From for DepositError { - fn from(e: ParseEventMessageError) -> Self { - Self::MessageParseFailed(e) - } - } + impl AsRef<[u8]> for DepositError { fn as_ref(&self) -> &[u8] { match self { @@ -823,32 +743,35 @@ pub mod error { Self::EventParseFailed(e) => e.as_ref(), Self::CustodianAddressMismatch => b"ERR_WRONG_EVENT_ADDRESS", Self::InsufficientAmountForFee => super::ERR_NOT_ENOUGH_BALANCE_FOR_FEE.as_bytes(), - Self::MessageParseFailed(e) => e.as_ref(), Self::InvalidAddress(e) => e.as_ref(), } } } - #[derive(Debug)] + #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] pub enum FinishDepositError { TransferCall(FtTransferCallError), ProofUsed, } + impl From for FinishDepositError { fn from(_: ProofUsed) -> Self { Self::ProofUsed } } + impl From for FinishDepositError { fn from(e: FtTransferCallError) -> Self { Self::TransferCall(e) } } + impl From for FinishDepositError { fn from(e: fungible_token::error::DepositError) -> Self { Self::TransferCall(FtTransferCallError::Transfer(e.into())) } } + impl AsRef<[u8]> for FinishDepositError { fn as_ref(&self) -> &[u8] { match self { @@ -862,11 +785,13 @@ pub mod error { Paused, FT(fungible_token::error::WithdrawError), } + impl From for WithdrawError { fn from(e: fungible_token::error::WithdrawError) -> Self { Self::FT(e) } } + impl AsRef<[u8]> for WithdrawError { fn as_ref(&self) -> &[u8] { match self { @@ -876,51 +801,39 @@ pub mod error { } } - #[derive(Debug)] - pub enum ParseOnTransferMessageError { - TooManyParts, - InvalidHexData, - WrongMessageFormat, - InvalidAccount, - } - impl AsRef<[u8]> for ParseOnTransferMessageError { - fn as_ref(&self) -> &[u8] { - match self { - Self::TooManyParts => b"ERR_INVALID_ON_TRANSFER_MESSAGE_FORMAT", - Self::InvalidHexData => b"ERR_INVALID_ON_TRANSFER_MESSAGE_HEX", - Self::WrongMessageFormat => b"ERR_INVALID_ON_TRANSFER_MESSAGE_DATA", - Self::InvalidAccount => INVALID_ACCOUNT, - } - } - } - - #[derive(Debug)] + #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] pub enum FtTransferCallError { + BalanceOverflow(BalanceOverflowError), MessageParseFailed(ParseOnTransferMessageError), InsufficientAmountForFee, Transfer(fungible_token::error::TransferError), } + impl From for FtTransferCallError { fn from(e: fungible_token::error::TransferError) -> Self { Self::Transfer(e) } } + impl From for FtTransferCallError { fn from(e: fungible_token::error::DepositError) -> Self { Self::Transfer(e.into()) } } + impl From for FtTransferCallError { fn from(e: ParseOnTransferMessageError) -> Self { Self::MessageParseFailed(e) } } + impl AsRef<[u8]> for FtTransferCallError { fn as_ref(&self) -> &[u8] { match self { Self::MessageParseFailed(e) => e.as_ref(), Self::InsufficientAmountForFee => super::ERR_NOT_ENOUGH_BALANCE_FOR_FEE.as_bytes(), Self::Transfer(e) => e.as_ref(), + Self::BalanceOverflow(e) => e.as_ref(), } } } @@ -929,6 +842,7 @@ pub mod error { AlreadyInitialized, InvalidCustodianAddress(AddressValidationError), } + impl AsRef<[u8]> for InitContractError { fn as_ref(&self) -> &[u8] { match self { @@ -939,6 +853,7 @@ pub mod error { } pub struct ProofUsed; + impl AsRef<[u8]> for ProofUsed { fn as_ref(&self) -> &[u8] { PROOF_EXIST diff --git a/engine/src/deposit_event.rs b/engine/src/deposit_event.rs index 495ad3b52..da1a9625f 100644 --- a/engine/src/deposit_event.rs +++ b/engine/src/deposit_event.rs @@ -1,11 +1,185 @@ +use crate::deposit_event::error::ParseEventMessageError; use crate::log_entry::LogEntry; -use crate::prelude::{vec, EthAddress, String, ToString, Vec, U256}; +use crate::prelude::account_id::AccountId; +use crate::prelude::{ + validate_eth_address, vec, AddressValidationError, Balance, BorshDeserialize, BorshSerialize, + EthAddress, Fee, String, ToString, TryFrom, TryInto, Vec, U256, +}; +use byte_slice_cast::AsByteSlice; use ethabi::{Event, EventParam, Hash, Log, ParamType, RawLog}; pub const DEPOSITED_EVENT: &str = "Deposited"; pub type EventParams = Vec; +/// On-transfer message. Used for `ft_transfer_call` and `ft_on_transfer` functions. +/// Message parsed from input args with `parse_on_transfer_message`. +#[derive(BorshSerialize, BorshDeserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub struct FtTransferMessageData { + pub relayer: AccountId, + pub recipient: EthAddress, + pub fee: Fee, +} + +impl FtTransferMessageData { + /// Get on-transfer data from arguments message field. + /// Used for `ft_transfer_call` and `ft_on_transfer` + pub fn parse_on_transfer_message( + message: &str, + ) -> Result { + // Split message by separator + let data: Vec<_> = message.split(':').collect(); + // Message data array should contain 2 elements + if data.len() != 2 { + return Err(error::ParseOnTransferMessageError::TooManyParts); + } + + // Check relayer account id from 1-th data element + let account_id = AccountId::try_from(data[0].as_bytes()) + .map_err(|_| error::ParseOnTransferMessageError::InvalidAccount)?; + + // Decode message array from 2-th element of data array + let msg = + hex::decode(data[1]).map_err(|_| error::ParseOnTransferMessageError::InvalidHexData)?; + // Length = fee[32] + eth_address[20] bytes + if msg.len() != 52 { + return Err(error::ParseOnTransferMessageError::WrongMessageFormat); + } + + // Parse fee from message slice. It should contain 32 bytes + // But after that in will be parse to u128 + // That logic for compatability. + let mut raw_fee: [u8; 32] = Default::default(); + raw_fee.copy_from_slice(&msg[..32]); + let fee_u128: u128 = U256::from_little_endian(&raw_fee) + .try_into() + .map_err(|_| error::ParseOnTransferMessageError::OverflowNumber)?; + let fee: Fee = fee_u128.into(); + + // Get recipient Eth address from message slice + let mut recipient: EthAddress = Default::default(); + recipient.copy_from_slice(&msg[32..52]); + + Ok(FtTransferMessageData { + relayer: account_id, + recipient, + fee, + }) + } + + /// Encode to String with specific rules + pub fn encode(&self) -> String { + // The first data section should contain fee data. + // Pay attention, that for compatibility reasons we used U256 type + // it means 32 bytes for fee data + let mut data = U256::from(self.fee.into_u128()).as_byte_slice().to_vec(); + // Second data section should contain Eth address + data.extend(self.recipient); + // Add `:` separator between relayer_id and data message + [self.relayer.as_ref(), &hex::encode(data)].join(":") + } + + /// Prepare message for `ft_transfer_call` -> `ft_on_transfer` + pub fn prepare_message_for_on_transfer( + relayer_account_id: &AccountId, + fee: Fee, + recipient: String, + ) -> Result { + // The first data section should contain fee data. + // Pay attention, that for compatibility reasons we used U256 type + // it means 32 bytes for fee data + let mut data = U256::from(fee.into_u128()).as_byte_slice().to_vec(); + + // Check message length. + let address = if recipient.len() == 42 { + recipient + .strip_prefix("0x") + .ok_or(ParseEventMessageError::EthAddressValidationError( + AddressValidationError::FailedDecodeHex, + ))? + .to_string() + } else { + recipient + }; + let recipient_address = validate_eth_address(address) + .map_err(ParseEventMessageError::EthAddressValidationError)?; + // Second data section should contain Eth address + data.extend(recipient_address); + // Add `:` separator between relayer_id and data message + //Ok([relayer_account_id.as_ref(), &hex::encode(data)].join(":")) + Ok(Self { + relayer: relayer_account_id.clone(), + recipient: recipient_address, + fee, + }) + } +} + +/// Token message data used for Deposit flow. +/// It contains two basic data structure: Near, Eth +/// The message parsed from event `recipient` field - `log_entry_data` +/// after fetching proof `log_entry_data` +#[derive(BorshSerialize, BorshDeserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub enum TokenMessageData { + /// Deposit no NEAR account + Near(AccountId), + ///Deposit to Eth accounts fee is being minted in the `ft_on_transfer` callback method + Eth { + receiver_id: AccountId, + message: FtTransferMessageData, + }, +} + +impl TokenMessageData { + /// Parse event message data for tokens. Data parsed form event `recipient` field. + /// Used for Deposit flow. + /// For Eth logic flow message validated and prepared for `ft_on_transfer` logic. + /// It mean validating Eth address correctness and preparing message for + /// parsing for `ft_on_transfer` message parsing with correct and validated data. + pub fn parse_event_message_and_prepare_token_message_data( + message: &str, + fee: Fee, + ) -> Result { + let data: Vec<_> = message.split(':').collect(); + // Data array can contain 1 or 2 elements + if data.len() >= 3 { + return Err(error::ParseEventMessageError::TooManyParts); + } + let account_id = AccountId::try_from(data[0].as_bytes()) + .map_err(|_| error::ParseEventMessageError::InvalidAccount)?; + + // If data array contain only one element it should return NEAR account id + if data.len() == 1 { + Ok(TokenMessageData::Near(account_id)) + } else { + let raw_message = data[1].into(); + let message = FtTransferMessageData::prepare_message_for_on_transfer( + &account_id, + fee, + raw_message, + )?; + + Ok(TokenMessageData::Eth { + receiver_id: account_id, + message, + }) + } + } + + // Get recipient account id from Eth part of Token message data + pub fn get_recipient(&self) -> AccountId { + match self { + Self::Near(acc) => acc.clone(), + Self::Eth { + receiver_id, + message: _, + } => receiver_id.clone(), + } + } +} + /// Ethereum event pub struct EthEvent { pub eth_custodian_address: EthAddress, @@ -45,13 +219,12 @@ impl EthEvent { } /// Data that was emitted by Deposited event. -#[derive(Debug, PartialEq)] pub struct DepositedEvent { pub eth_custodian_address: EthAddress, pub sender: EthAddress, - pub recipient: String, - pub amount: U256, - pub fee: U256, + pub token_message_data: TokenMessageData, + pub amount: Balance, + pub fee: Fee, } impl DepositedEvent { @@ -91,21 +264,36 @@ impl DepositedEvent { .into_address() .ok_or(error::ParseError::InvalidSender)? .0; - let recipient: String = event.log.params[1].value.clone().to_string(); - let amount = event.log.params[2] + + // parse_event_message + let event_message_data: String = event.log.params[1].value.clone().to_string(); + + let amount: u128 = event.log.params[2] .value .clone() .into_uint() - .ok_or(error::ParseError::InvalidAmount)?; - let fee = event.log.params[3] + .ok_or(error::ParseError::InvalidAmount)? + .try_into() + .map_err(|_| error::ParseError::OverflowNumber)?; + let raw_fee: u128 = event.log.params[3] .value .clone() .into_uint() - .ok_or(error::ParseError::InvalidFee)?; + .ok_or(error::ParseError::InvalidFee)? + .try_into() + .map_err(|_| error::ParseError::OverflowNumber)?; + let fee: Fee = raw_fee.into(); + + let token_message_data = + TokenMessageData::parse_event_message_and_prepare_token_message_data( + &event_message_data, + fee, + )?; + Ok(Self { eth_custodian_address: event.eth_custodian_address, sender, - recipient, + token_message_data, amount, fee, }) @@ -113,6 +301,8 @@ impl DepositedEvent { } pub mod error { + use super::*; + #[derive(Debug)] pub enum DecodeError { RlpFailed, @@ -127,12 +317,39 @@ pub mod error { } } - #[derive(Debug)] + #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] + pub enum ParseEventMessageError { + TooManyParts, + InvalidAccount, + EthAddressValidationError(AddressValidationError), + ParseMessageError(ParseOnTransferMessageError), + } + + impl AsRef<[u8]> for ParseEventMessageError { + fn as_ref(&self) -> &[u8] { + match self { + Self::TooManyParts => b"ERR_INVALID_EVENT_MESSAGE_FORMAT", + Self::InvalidAccount => b"ERR_INVALID_ACCOUNT_ID", + Self::EthAddressValidationError(e) => e.as_ref(), + Self::ParseMessageError(e) => e.as_ref(), + } + } + } + + impl From for ParseError { + fn from(e: ParseEventMessageError) -> Self { + Self::MessageParseFailed(e) + } + } + + #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] pub enum ParseError { LogParseFailed(DecodeError), InvalidSender, InvalidAmount, InvalidFee, + MessageParseFailed(ParseEventMessageError), + OverflowNumber, } impl AsRef<[u8]> for ParseError { fn as_ref(&self) -> &[u8] { @@ -141,6 +358,29 @@ pub mod error { Self::InvalidSender => b"ERR_INVALID_SENDER", Self::InvalidAmount => b"ERR_INVALID_AMOUNT", Self::InvalidFee => b"ERR_INVALID_FEE", + Self::MessageParseFailed(e) => e.as_ref(), + Self::OverflowNumber => b"ERR_OVERFLOW_NUMBER", + } + } + } + + #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] + pub enum ParseOnTransferMessageError { + TooManyParts, + InvalidHexData, + WrongMessageFormat, + InvalidAccount, + OverflowNumber, + } + + impl AsRef<[u8]> for ParseOnTransferMessageError { + fn as_ref(&self) -> &[u8] { + match self { + Self::TooManyParts => b"ERR_INVALID_ON_TRANSFER_MESSAGE_FORMAT", + Self::InvalidHexData => b"ERR_INVALID_ON_TRANSFER_MESSAGE_HEX", + Self::WrongMessageFormat => b"ERR_INVALID_ON_TRANSFER_MESSAGE_DATA", + Self::InvalidAccount => b"ERR_INVALID_ACCOUNT_ID", + Self::OverflowNumber => b"ERR_OVERFLOW_NUMBER", } } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs index a54a6e9f0..22304742e 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -227,11 +227,13 @@ impl AsRef<[u8]> for DeployErc20Error { } pub struct ERC20Address(Address); + impl AsRef<[u8]> for ERC20Address { fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } + impl TryFrom> for ERC20Address { type Error = AddressParseError; @@ -243,7 +245,9 @@ impl TryFrom> for ERC20Address { } } } + pub struct AddressParseError; + impl AsRef<[u8]> for AddressParseError { fn as_ref(&self) -> &[u8] { b"ERR_PARSE_ADDRESS" @@ -251,11 +255,13 @@ impl AsRef<[u8]> for AddressParseError { } pub struct NEP141Account(AccountId); + impl AsRef<[u8]> for NEP141Account { fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } + impl TryFrom> for NEP141Account { type Error = aurora_engine_types::account_id::ParseAccountError; @@ -397,7 +403,6 @@ pub struct Engine<'env, I: IO, E: Env> { env: &'env E, } -// TODO: upgrade to Berlin HF pub(crate) const CONFIG: &Config = &Config::london(); /// Key for storing the state of the engine. @@ -1137,12 +1142,14 @@ pub fn set_balance(io: &mut I, address: &Address, balance: &Wei) { } pub fn remove_balance(io: &mut I, address: &Address) { - let balance = get_balance(io, address); + // The `unwrap` is safe here because if the connector + // is implemented correctly then the "Eth on Aurora" wll never underflow. + let balance = get_balance(io, address).try_into_u128().unwrap(); // Apply changes for eth-connector. The `unwrap` is safe here because (a) if the connector // is implemented correctly then the total supply wll never underflow and (b) we are passing // in the balance directly so there will always be enough balance. - EthConnectorContract::get_instance(*io) - .internal_remove_eth(address, &balance.raw()) + EthConnectorContract::init_instance(*io) + .internal_remove_eth(address, balance) .unwrap(); io.remove_storage(&address_to_key(KeyPrefix::Balance, address)); } diff --git a/engine/src/fungible_token.rs b/engine/src/fungible_token.rs index 3d7ad8c7c..78644eb32 100644 --- a/engine/src/fungible_token.rs +++ b/engine/src/fungible_token.rs @@ -1,17 +1,15 @@ -use crate::connector::NO_DEPOSIT; +use crate::connector::ZERO_ATTACHED_BALANCE; use crate::engine; use crate::json::{parse_json, JsonValue}; use crate::parameters::{NEP141FtOnTransferArgs, ResolveTransferCallArgs, StorageBalance}; use crate::prelude::account_id::AccountId; use crate::prelude::{ sdk, storage, vec, Address, BTreeMap, Balance, BorshDeserialize, BorshSerialize, EthAddress, - NearGas, PromiseResult, StorageBalanceBounds, StorageUsage, String, ToString, TryInto, Vec, + NearGas, PromiseAction, PromiseBatchAction, PromiseCreateArgs, PromiseResult, + PromiseWithCallbackArgs, StorageBalanceBounds, StorageUsage, String, ToString, TryInto, Vec, Wei, U256, }; use aurora_engine_sdk::io::{StorageIntermediate, IO}; -use aurora_engine_types::parameters::{ - PromiseAction, PromiseBatchAction, PromiseCreateArgs, PromiseWithCallbackArgs, -}; const GAS_FOR_RESOLVE_TRANSFER: NearGas = NearGas::new(5_000_000_000_000); const GAS_FOR_FT_ON_TRANSFER: NearGas = NearGas::new(10_000_000_000_000); @@ -52,6 +50,24 @@ pub struct FungibleTokenOps { io: I, } +/// Fungible token Reference hash type. +/// Used for FungibleTokenMetadata +#[derive(BorshDeserialize, BorshSerialize, Clone)] +pub struct FungibleReferenceHash([u8; 32]); + +impl FungibleReferenceHash { + /// Encode to base64-encoded string + pub fn encode(&self) -> String { + base64::encode(self) + } +} + +impl AsRef<[u8]> for FungibleReferenceHash { + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + #[derive(BorshDeserialize, BorshSerialize, Clone)] pub struct FungibleTokenMetadata { pub spec: String, @@ -59,7 +75,7 @@ pub struct FungibleTokenMetadata { pub symbol: String, pub icon: Option, pub reference: Option, - pub reference_hash: Option<[u8; 32]>, + pub reference_hash: Option, pub decimals: u8, } @@ -101,7 +117,7 @@ impl From for JsonValue { "reference_hash".to_string(), metadata .reference_hash - .map(|hash| JsonValue::String(base64::encode(hash))) + .map(|hash| JsonValue::String(hash.encode())) .unwrap_or(JsonValue::Null), ); kvs.insert( @@ -127,10 +143,11 @@ impl FungibleTokenOps { } /// Balance of ETH (ETH on Aurora) - pub fn internal_unwrap_balance_of_eth_on_aurora(&self, address: EthAddress) -> Balance { - engine::get_balance(&self.io, &Address(address)) - .raw() - .as_u128() + pub fn internal_unwrap_balance_of_eth_on_aurora( + &self, + address: EthAddress, + ) -> Result { + engine::get_balance(&self.io, &Address(address)).try_into_u128() } /// Internal ETH deposit to NEAR - nETH (NEP-141) @@ -157,7 +174,9 @@ impl FungibleTokenOps { address: EthAddress, amount: Balance, ) -> Result<(), error::DepositError> { - let balance = self.internal_unwrap_balance_of_eth_on_aurora(address); + let balance = self + .internal_unwrap_balance_of_eth_on_aurora(address) + .map_err(|_| error::DepositError::BalanceOverflow)?; let new_balance = balance .checked_add(amount) .ok_or(error::DepositError::BalanceOverflow)?; @@ -197,7 +216,9 @@ impl FungibleTokenOps { address: EthAddress, amount: Balance, ) -> Result<(), error::WithdrawError> { - let balance = self.internal_unwrap_balance_of_eth_on_aurora(address); + let balance = self + .internal_unwrap_balance_of_eth_on_aurora(address) + .map_err(error::WithdrawError::BalanceOverflow)?; let new_balance = balance .checked_sub(amount) .ok_or(error::WithdrawError::InsufficientFunds)?; @@ -227,8 +248,12 @@ impl FungibleTokenOps { if amount == 0 { return Err(error::TransferError::ZeroAmount); } + + // Check is account receiver_id exist if !self.accounts_contains_key(receiver_id) { - // TODO: how does this interact with the storage deposit concept? + // Register receiver_id account with 0 balance. We need it because + // when we retire to get the balance of `receiver_id` it will fail + // if it does not exist. self.internal_register_account(receiver_id) } self.internal_withdraw_eth_from_near(sender_id, amount)?; @@ -250,15 +275,15 @@ impl FungibleTokenOps { self.accounts_insert(account_id, 0) } - pub fn ft_total_eth_supply_on_near(&self) -> u128 { + pub fn ft_total_eth_supply_on_near(&self) -> Balance { self.total_eth_supply_on_near } - pub fn ft_total_eth_supply_on_aurora(&self) -> u128 { + pub fn ft_total_eth_supply_on_aurora(&self) -> Balance { self.total_eth_supply_on_aurora } - pub fn ft_balance_of(&self, account_id: &AccountId) -> u128 { + pub fn ft_balance_of(&self, account_id: &AccountId) -> Balance { self.get_account_eth_balance(account_id).unwrap_or(0) } @@ -295,14 +320,14 @@ impl FungibleTokenOps { target_account_id: receiver_id, method: "ft_on_transfer".to_string(), args: data1.into_bytes(), - attached_balance: NO_DEPOSIT, + attached_balance: ZERO_ATTACHED_BALANCE, attached_gas: GAS_FOR_FT_ON_TRANSFER.into_u64(), }; let ft_resolve_transfer_call = PromiseCreateArgs { target_account_id: current_account_id, method: "ft_resolve_transfer".to_string(), args: data2, - attached_balance: NO_DEPOSIT, + attached_balance: ZERO_ATTACHED_BALANCE, attached_gas: GAS_FOR_RESOLVE_TRANSFER.into_u64(), }; Ok(PromiseWithCallbackArgs { @@ -317,7 +342,7 @@ impl FungibleTokenOps { sender_id: &AccountId, receiver_id: &AccountId, amount: Balance, - ) -> (u128, u128) { + ) -> (Balance, Balance) { // Get the unused amount from the `ft_on_transfer` call result. let unused_amount = match promise_result { PromiseResult::NotReady => unreachable!(), @@ -382,8 +407,8 @@ impl FungibleTokenOps { promise_result: PromiseResult, sender_id: &AccountId, receiver_id: &AccountId, - amount: u128, - ) -> u128 { + amount: Balance, + ) -> Balance { self.internal_ft_resolve_transfer(promise_result, sender_id, receiver_id, amount) .0 } @@ -567,6 +592,8 @@ impl FungibleTokenOps { } pub mod error { + use crate::prelude::types::error::BalanceOverflowError; + const TOTAL_SUPPLY_OVERFLOW: &[u8; 25] = b"ERR_TOTAL_SUPPLY_OVERFLOW"; const BALANCE_OVERFLOW: &[u8; 20] = b"ERR_BALANCE_OVERFLOW"; const NOT_ENOUGH_BALANCE: &[u8; 22] = b"ERR_NOT_ENOUGH_BALANCE"; @@ -579,6 +606,7 @@ pub mod error { TotalSupplyOverflow, BalanceOverflow, } + impl AsRef<[u8]> for DepositError { fn as_ref(&self) -> &[u8] { match self { @@ -592,12 +620,15 @@ pub mod error { pub enum WithdrawError { TotalSupplyUnderflow, InsufficientFunds, + BalanceOverflow(BalanceOverflowError), } + impl AsRef<[u8]> for WithdrawError { fn as_ref(&self) -> &[u8] { match self { Self::TotalSupplyUnderflow => TOTAL_SUPPLY_UNDERFLOW, Self::InsufficientFunds => NOT_ENOUGH_BALANCE, + Self::BalanceOverflow(e) => e.as_ref(), } } } @@ -611,6 +642,7 @@ pub mod error { ZeroAmount, SelfTransfer, } + impl AsRef<[u8]> for TransferError { fn as_ref(&self) -> &[u8] { match self { @@ -629,9 +661,11 @@ pub mod error { match err { WithdrawError::InsufficientFunds => Self::InsufficientFunds, WithdrawError::TotalSupplyUnderflow => Self::TotalSupplyUnderflow, + WithdrawError::BalanceOverflow(_) => Self::BalanceOverflow, } } } + impl From for TransferError { fn from(err: DepositError) -> Self { match err { @@ -648,6 +682,7 @@ pub mod error { InsufficientDeposit, UnRegisterPositiveBalance, } + impl AsRef<[u8]> for StorageFundingError { fn as_ref(&self) -> &[u8] { match self { diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 09b69ced4..7079cadeb 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -336,7 +336,7 @@ mod contract { .sdk_unwrap(); if predecessor_account_id == current_account_id { - EthConnectorContract::get_instance(io) + EthConnectorContract::init_instance(io) .ft_on_transfer(&engine, &args) .sdk_unwrap(); } else { @@ -439,6 +439,7 @@ mod contract { } } } + /// /// NONMUTATIVE METHODS /// @@ -548,7 +549,7 @@ mod contract { let args: InitCallArgs = io.read_input_borsh().sdk_unwrap(); let owner_id = io.current_account_id(); - EthConnectorContract::init_contract(io, owner_id, args).sdk_unwrap(); + EthConnectorContract::create_contract(io, owner_id, args).sdk_unwrap(); } #[no_mangle] @@ -568,7 +569,7 @@ mod contract { let args = io.read_input_borsh().sdk_unwrap(); let current_account_id = io.current_account_id(); let predecessor_account_id = io.predecessor_account_id(); - let result = EthConnectorContract::get_instance(io) + let result = EthConnectorContract::init_instance(io) .withdraw_eth_from_near(¤t_account_id, &predecessor_account_id, args) .sdk_unwrap(); let result_bytes = result.try_to_vec().sdk_expect("ERR_SERIALIZE"); @@ -581,7 +582,7 @@ mod contract { let raw_proof = io.read_input().to_vec(); let current_account_id = io.current_account_id(); let predecessor_account_id = io.predecessor_account_id(); - let promise_args = EthConnectorContract::get_instance(io) + let promise_args = EthConnectorContract::init_instance(io) .deposit(raw_proof, current_account_id, predecessor_account_id) .sdk_unwrap(); let promise_id = io.promise_crate_with_callback(&promise_args); @@ -610,7 +611,7 @@ mod contract { let data = io.read_input_borsh().sdk_unwrap(); let current_account_id = io.current_account_id(); let predecessor_account_id = io.predecessor_account_id(); - let maybe_promise_args = EthConnectorContract::get_instance(io) + let maybe_promise_args = EthConnectorContract::init_instance(io) .finish_deposit(predecessor_account_id, current_account_id, data) .sdk_unwrap(); @@ -625,7 +626,7 @@ mod contract { let mut io = Runtime; let args: IsUsedProofCallArgs = io.read_input_borsh().sdk_unwrap(); - let is_used_proof = EthConnectorContract::get_instance(io).is_used_proof(args.proof); + let is_used_proof = EthConnectorContract::init_instance(io).is_used_proof(args.proof); let res = is_used_proof.try_to_vec().unwrap(); io.return_output(&res[..]); } @@ -633,19 +634,19 @@ mod contract { #[no_mangle] pub extern "C" fn ft_total_supply() { let io = Runtime; - EthConnectorContract::get_instance(io).ft_total_eth_supply_on_near(); + EthConnectorContract::init_instance(io).ft_total_eth_supply_on_near(); } #[no_mangle] pub extern "C" fn ft_total_eth_supply_on_near() { let io = Runtime; - EthConnectorContract::get_instance(io).ft_total_eth_supply_on_near(); + EthConnectorContract::init_instance(io).ft_total_eth_supply_on_near(); } #[no_mangle] pub extern "C" fn ft_total_eth_supply_on_aurora() { let io = Runtime; - EthConnectorContract::get_instance(io).ft_total_eth_supply_on_aurora(); + EthConnectorContract::init_instance(io).ft_total_eth_supply_on_aurora(); } #[no_mangle] @@ -655,14 +656,16 @@ mod contract { parse_json(&io.read_input().to_vec()).sdk_unwrap(), ) .sdk_unwrap(); - EthConnectorContract::get_instance(io).ft_balance_of(args); + EthConnectorContract::init_instance(io).ft_balance_of(args); } #[no_mangle] pub extern "C" fn ft_balance_of_eth() { let io = Runtime; let args: parameters::BalanceOfEthCallArgs = io.read_input().to_value().sdk_unwrap(); - EthConnectorContract::get_instance(io).ft_balance_of_eth_on_aurora(args); + EthConnectorContract::init_instance(io) + .ft_balance_of_eth_on_aurora(args) + .sdk_unwrap(); } #[no_mangle] @@ -674,7 +677,7 @@ mod contract { parse_json(&io.read_input().to_vec()).sdk_unwrap(), ) .sdk_unwrap(); - EthConnectorContract::get_instance(io) + EthConnectorContract::init_instance(io) .ft_transfer(&predecessor_account_id, args) .sdk_unwrap(); } @@ -691,7 +694,7 @@ mod contract { let args: ResolveTransferCallArgs = io.read_input().to_value().sdk_unwrap(); let promise_result = io.promise_result(0).sdk_unwrap(); - EthConnectorContract::get_instance(io).ft_resolve_transfer(args, promise_result); + EthConnectorContract::init_instance(io).ft_resolve_transfer(args, promise_result); } #[no_mangle] @@ -707,7 +710,7 @@ mod contract { .sdk_unwrap(); let current_account_id = io.current_account_id(); let predecessor_account_id = io.predecessor_account_id(); - let promise_args = EthConnectorContract::get_instance(io) + let promise_args = EthConnectorContract::init_instance(io) .ft_transfer_call(predecessor_account_id, current_account_id, args) .sdk_unwrap(); let promise_id = io.promise_crate_with_callback(&promise_args); @@ -720,7 +723,7 @@ mod contract { let args = StorageDepositCallArgs::from(parse_json(&io.read_input().to_vec()).sdk_unwrap()); let predecessor_account_id = io.predecessor_account_id(); let amount = io.attached_deposit(); - let maybe_promise = EthConnectorContract::get_instance(io) + let maybe_promise = EthConnectorContract::init_instance(io) .storage_deposit(predecessor_account_id, amount, args) .sdk_unwrap(); if let Some(promise) = maybe_promise { @@ -734,7 +737,7 @@ mod contract { io.assert_one_yocto().sdk_unwrap(); let predecessor_account_id = io.predecessor_account_id(); let force = parse_json(&io.read_input().to_vec()).and_then(|args| args.bool("force").ok()); - let maybe_promise = EthConnectorContract::get_instance(io) + let maybe_promise = EthConnectorContract::init_instance(io) .storage_unregister(predecessor_account_id, force) .sdk_unwrap(); if let Some(promise) = maybe_promise { @@ -749,7 +752,7 @@ mod contract { let args = StorageWithdrawCallArgs::from(parse_json(&io.read_input().to_vec()).sdk_unwrap()); let predecessor_account_id = io.predecessor_account_id(); - EthConnectorContract::get_instance(io) + EthConnectorContract::init_instance(io) .storage_withdraw(&predecessor_account_id, args) .sdk_unwrap() } @@ -761,13 +764,13 @@ mod contract { parse_json(&io.read_input().to_vec()).sdk_unwrap(), ) .sdk_unwrap(); - EthConnectorContract::get_instance(io).storage_balance_of(args) + EthConnectorContract::init_instance(io).storage_balance_of(args) } #[no_mangle] pub extern "C" fn get_paused_flags() { let mut io = Runtime; - let paused_flags = EthConnectorContract::get_instance(io).get_paused_flags(); + let paused_flags = EthConnectorContract::init_instance(io).get_paused_flags(); let data = paused_flags.try_to_vec().expect(ERR_FAILED_PARSE); io.return_output(&data[..]); } @@ -778,13 +781,13 @@ mod contract { io.assert_private_call().sdk_unwrap(); let args: PauseEthConnectorCallArgs = io.read_input_borsh().sdk_unwrap(); - EthConnectorContract::get_instance(io).set_paused_flags(args); + EthConnectorContract::init_instance(io).set_paused_flags(args); } #[no_mangle] pub extern "C" fn get_accounts_counter() { let io = Runtime; - EthConnectorContract::get_instance(io).get_accounts_counter(); + EthConnectorContract::init_instance(io).get_accounts_counter(); } #[no_mangle] @@ -861,7 +864,7 @@ mod contract { amount: balance.low_u128(), proof_key: crate::prelude::String::new(), relayer_id: aurora_account_id.clone(), - fee: 0, + fee: 0.into(), msg: None, }; let verify_call = aurora_engine_types::parameters::PromiseCreateArgs { diff --git a/engine/src/parameters.rs b/engine/src/parameters.rs index acd64996e..d27ce394b 100644 --- a/engine/src/parameters.rs +++ b/engine/src/parameters.rs @@ -7,6 +7,7 @@ use crate::prelude::{ String, ToString, TryFrom, Vec, WeiU256, }; use crate::proof::Proof; +use aurora_engine_types::types::Fee; use evm::backend::Log; /// Borsh-encoded parameters for the `new` function. @@ -160,12 +161,12 @@ impl CallArgs { // made for flexibility and extensibility. if let Ok(value) = Self::try_from_slice(bytes) { Some(value) - // Fallback, for handling old input format, - // i.e. input, formed as a raw (not wrapped into call args enum) data structure with legacy arguments, - // made for backward compatibility. + // Fallback, for handling old input format, + // i.e. input, formed as a raw (not wrapped into call args enum) data structure with legacy arguments, + // made for backward compatibility. } else if let Ok(value) = FunctionCallArgsV1::try_from_slice(bytes) { Some(Self::V1(value)) - // Dealing with unrecognized input should be handled and result as an exception in a call site. + // Dealing with unrecognized input should be handled and result as an exception in a call site. } else { None } @@ -324,7 +325,7 @@ pub struct FinishDepositCallArgs { pub amount: Balance, pub proof_key: String, pub relayer_id: AccountId, - pub fee: Balance, + pub fee: Fee, pub msg: Option>, } @@ -500,16 +501,19 @@ pub mod error { Json(JsonError), InvalidAccount(ParseAccountError), } + impl From for ParseTypeFromJsonError { fn from(e: JsonError) -> Self { Self::Json(e) } } + impl From for ParseTypeFromJsonError { fn from(e: ParseAccountError) -> Self { Self::InvalidAccount(e) } } + impl AsRef<[u8]> for ParseTypeFromJsonError { fn as_ref(&self) -> &[u8] { match self { diff --git a/etc/state-migration-test/Cargo.lock b/etc/state-migration-test/Cargo.lock index 402d4b124..c190f43cf 100644 --- a/etc/state-migration-test/Cargo.lock +++ b/etc/state-migration-test/Cargo.lock @@ -72,7 +72,6 @@ dependencies = [ "base64", "blake2", "borsh", - "byte-slice-cast", "ethabi", "evm", "evm-core",