diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab6020c29..1323b64ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --verbose + args: --locked --verbose env: CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 diff --git a/src/benches/eth_deploy_code.rs b/src/benches/eth_deploy_code.rs index 1c0714ca5..565dd01bc 100644 --- a/src/benches/eth_deploy_code.rs +++ b/src/benches/eth_deploy_code.rs @@ -2,10 +2,11 @@ use criterion::{BatchSize, BenchmarkId, Criterion, Throughput}; use secp256k1::SecretKey; use crate::test_utils::{address_from_secret_key, create_eth_transaction, deploy_evm, SUBMIT}; +use crate::types::Wei; -const INITIAL_BALANCE: u64 = 1000; +const INITIAL_BALANCE: Wei = Wei::new_u64(1000); const INITIAL_NONCE: u64 = 0; -const TRANSFER_AMOUNT: u64 = 0; +const TRANSFER_AMOUNT: Wei = Wei::zero(); pub(crate) fn eth_deploy_code_benchmark(c: &mut Criterion) { let mut runner = deploy_evm(); @@ -13,7 +14,7 @@ pub(crate) fn eth_deploy_code_benchmark(c: &mut Criterion) { let source_account = SecretKey::random(&mut rng); runner.create_address( address_from_secret_key(&source_account), - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); let inputs: Vec<_> = [1, 4, 8, 12, 16] diff --git a/src/benches/eth_erc20.rs b/src/benches/eth_erc20.rs index 4dbca6449..d8f2424a1 100644 --- a/src/benches/eth_erc20.rs +++ b/src/benches/eth_erc20.rs @@ -15,7 +15,7 @@ pub(crate) fn eth_erc20_benchmark(c: &mut Criterion) { let source_account = SecretKey::random(&mut rng); runner.create_address( address_from_secret_key(&source_account), - INITIAL_BALANCE.into(), + crate::types::Wei::new_u64(INITIAL_BALANCE), INITIAL_NONCE.into(), ); let calling_account_id = "some-account.near".to_string(); diff --git a/src/benches/eth_standard_precompiles.rs b/src/benches/eth_standard_precompiles.rs index 8e6449943..fd57ce697 100644 --- a/src/benches/eth_standard_precompiles.rs +++ b/src/benches/eth_standard_precompiles.rs @@ -4,8 +4,9 @@ use secp256k1::SecretKey; use crate::test_utils::standard_precompiles::{PrecompilesConstructor, PrecompilesContract}; use crate::test_utils::{address_from_secret_key, deploy_evm, sign_transaction, SUBMIT}; +use crate::types::Wei; -const INITIAL_BALANCE: u64 = 1000; +const INITIAL_BALANCE: Wei = Wei::new_u64(1000); const INITIAL_NONCE: u64 = 0; pub(crate) fn eth_standard_precompiles_benchmark(c: &mut Criterion) { @@ -14,7 +15,7 @@ pub(crate) fn eth_standard_precompiles_benchmark(c: &mut Criterion) { let source_account = SecretKey::random(&mut rng); runner.create_address( address_from_secret_key(&source_account), - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); let calling_account_id = "some-account.near".to_string(); diff --git a/src/benches/eth_transfer.rs b/src/benches/eth_transfer.rs index 5532d07d5..2a0ec6c22 100644 --- a/src/benches/eth_transfer.rs +++ b/src/benches/eth_transfer.rs @@ -2,10 +2,11 @@ use criterion::{BatchSize, Criterion}; use secp256k1::SecretKey; use crate::test_utils::{address_from_secret_key, create_eth_transaction, deploy_evm, SUBMIT}; +use crate::types::Wei; -const INITIAL_BALANCE: u64 = 1000; +const INITIAL_BALANCE: Wei = Wei::new_u64(1000); const INITIAL_NONCE: u64 = 0; -const TRANSFER_AMOUNT: u64 = 123; +const TRANSFER_AMOUNT: Wei = Wei::new_u64(123); pub(crate) fn eth_transfer_benchmark(c: &mut Criterion) { let mut runner = deploy_evm(); @@ -13,13 +14,13 @@ pub(crate) fn eth_transfer_benchmark(c: &mut Criterion) { let source_account = SecretKey::random(&mut rng); runner.create_address( address_from_secret_key(&source_account), - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); let dest_account = address_from_secret_key(&SecretKey::random(&mut rng)); let transaction = create_eth_transaction( Some(dest_account), - TRANSFER_AMOUNT.into(), + TRANSFER_AMOUNT, vec![], Some(runner.chain_id), &source_account, diff --git a/src/engine.rs b/src/engine.rs index a1190b97d..2383ce10a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -10,7 +10,7 @@ use crate::precompiles; use crate::prelude::{Address, TryInto, Vec, H256, U256}; use crate::sdk; use crate::storage::{address_to_key, bytes_to_key, storage_to_key, KeyPrefix, KeyPrefixU8}; -use crate::types::{u256_to_arr, AccountId}; +use crate::types::{u256_to_arr, AccountId, Wei}; /// Errors with the EVM engine. #[derive(Debug, Clone, Eq, PartialEq)] @@ -92,6 +92,20 @@ impl ExitIntoResult for ExitReason { } } +pub enum EngineStateError { + NotFound, + DeserializationFailed, +} + +impl AsRef<[u8]> for EngineStateError { + fn as_ref(&self) -> &[u8] { + match self { + Self::NotFound => b"ERR_STATE_NOT_FOUND", + Self::DeserializationFailed => b"ERR_STATE_CORRUPTED", + } + } +} + /// Engine internal state, mostly configuration. /// Should not contain anything large or enumerable. #[derive(BorshSerialize, BorshDeserialize, Default)] @@ -134,8 +148,8 @@ const CONFIG: &Config = &Config::istanbul(); const STATE_KEY: &[u8; 5] = b"STATE"; impl Engine { - pub fn new(origin: Address) -> Self { - Self::new_with_state(Engine::get_state(), origin) + pub fn new(origin: Address) -> Result { + Engine::get_state().map(|state| Self::new_with_state(state, origin)) } pub fn new_with_state(state: EngineState, origin: Address) -> Self { @@ -151,10 +165,11 @@ impl Engine { } /// Fails if state is not found. - pub fn get_state() -> EngineState { + pub fn get_state() -> Result { match sdk::read_storage(&bytes_to_key(KeyPrefix::Config, STATE_KEY)) { - None => Default::default(), - Some(bytes) => EngineState::try_from_slice(&bytes).expect("ERR_DESER"), + None => Err(EngineStateError::NotFound), + Some(bytes) => EngineState::try_from_slice(&bytes) + .map_err(|_| EngineStateError::DeserializationFailed), } } @@ -171,8 +186,7 @@ impl Engine { } pub fn get_code_size(address: &Address) -> usize { - // TODO: Seems this can be optimized to only read the register length. - Engine::get_code(&address).len() + sdk::read_storage_len(&address_to_key(KeyPrefix::Code, address)).unwrap_or(0) } pub fn set_nonce(address: &Address, nonce: &U256) { @@ -205,10 +219,10 @@ impl Engine { .unwrap_or_else(U256::zero) } - pub fn set_balance(address: &Address, balance: &U256) { + pub fn set_balance(address: &Address, balance: &Wei) { sdk::write_storage( &address_to_key(KeyPrefix::Balance, address), - &u256_to_arr(balance), + &balance.to_bytes(), ); } @@ -216,10 +230,11 @@ impl Engine { sdk::remove_storage(&address_to_key(KeyPrefix::Balance, address)) } - pub fn get_balance(address: &Address) -> U256 { - sdk::read_storage(&address_to_key(KeyPrefix::Balance, address)) + pub fn get_balance(address: &Address) -> Wei { + let raw = sdk::read_storage(&address_to_key(KeyPrefix::Balance, address)) .map(|value| U256::from_big_endian(&value)) - .unwrap_or_else(U256::zero) + .unwrap_or_else(U256::zero); + Wei::new(raw) } pub fn remove_storage(address: &Address, key: &H256) { @@ -240,7 +255,7 @@ impl Engine { let balance = Self::get_balance(address); let nonce = Self::get_nonce(address); let code_len = Self::get_code_size(address); - balance == U256::zero() && nonce == U256::zero() && code_len == 0 + balance.is_zero() && nonce.is_zero() && code_len == 0 } /// Removes all storage for the given address. @@ -272,20 +287,20 @@ impl Engine { pub fn deploy_code_with_input(&mut self, input: Vec) -> EngineResult { let origin = self.origin(); - let value = U256::zero(); + let value = Wei::zero(); self.deploy_code(origin, value, input) } pub fn deploy_code( &mut self, origin: Address, - value: U256, + value: Wei, input: Vec, ) -> EngineResult { let mut executor = self.make_executor(); let address = executor.create_address(CreateScheme::Legacy { caller: origin }); let (status, result) = ( - executor.transact_create(origin, value, input, u64::MAX), + executor.transact_create(origin, value.raw(), input, u64::MAX), address, ); @@ -306,7 +321,7 @@ impl Engine { pub fn call_with_args(&mut self, args: FunctionCallArgs) -> EngineResult { let origin = self.origin(); let contract = Address(args.contract); - let value = U256::zero(); + let value = Wei::zero(); self.call(origin, contract, value, args.input) } @@ -314,11 +329,12 @@ impl Engine { &mut self, origin: Address, contract: Address, - value: U256, + value: Wei, input: Vec, ) -> EngineResult { let mut executor = self.make_executor(); - let (status, result) = executor.transact_call(origin, contract, value, input, u64::MAX); + let (status, result) = + executor.transact_call(origin, contract, value.raw(), input, u64::MAX); let used_gas = executor.used_gas(); let (values, logs) = executor.into_state().deconstruct(); @@ -362,18 +378,19 @@ impl Engine { let origin = Address::from_slice(&args.sender); let contract = Address::from_slice(&args.address); let value = U256::from_big_endian(&args.amount); - self.view(origin, contract, value, args.input) + self.view(origin, contract, Wei::new(value), args.input) } pub fn view( &self, origin: Address, contract: Address, - value: U256, + value: Wei, input: Vec, ) -> EngineResult> { let mut executor = self.make_executor(); - let (status, result) = executor.transact_call(origin, contract, value, input, u64::MAX); + let (status, result) = + executor.transact_call(origin, contract, value.raw(), input, u64::MAX); status.into_result()?; Ok(result) } @@ -492,7 +509,7 @@ impl evm::backend::Backend for Engine { fn basic(&self, address: Address) -> Basic { Basic { nonce: Engine::get_nonce(&address), - balance: Engine::get_balance(&address), + balance: Engine::get_balance(&address).raw(), } } @@ -531,7 +548,7 @@ impl ApplyBackend for Engine { reset_storage, } => { Engine::set_nonce(&address, &basic.nonce); - Engine::set_balance(&address, &basic.balance); + Engine::set_balance(&address, &Wei::new(basic.balance)); if let Some(code) = code { Engine::set_code(&address, &code) } diff --git a/src/lib.rs b/src/lib.rs index 6ef419227..5132b9856 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,13 +35,13 @@ mod tests; #[cfg(feature = "contract")] mod contract { - use borsh::{BorshDeserialize, BorshSerialize}; + use borsh::BorshSerialize; use crate::engine::{Engine, EngineResult, EngineState}; #[cfg(feature = "evm_bully")] use crate::parameters::{BeginBlockArgs, BeginChainArgs}; use crate::parameters::{FunctionCallArgs, GetStorageAtArgs, NewCallArgs, ViewCallArgs}; - use crate::prelude::{Address, TryInto, H256, U256}; + use crate::prelude::{Address, H256, U256}; use crate::sdk; use crate::storage::{bytes_to_key, KeyPrefix}; use crate::types::{near_account_to_evm_address, u256_to_arr}; @@ -74,11 +74,11 @@ mod contract { /// Should be called on deployment. #[no_mangle] pub extern "C" fn new() { - let state = Engine::get_state(); - if !state.owner_id.is_empty() { + if let Ok(state) = Engine::get_state() { require_owner_only(&state); } - let args = NewCallArgs::try_from_slice(&sdk::read_input()).sdk_expect("ERR_ARG_PARSE"); + + let args: NewCallArgs = sdk::read_input_borsh().sdk_unwrap(); Engine::set_state(args.into()); } @@ -95,35 +95,36 @@ mod contract { /// Get owner account id for this contract. #[no_mangle] pub extern "C" fn get_owner() { - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); sdk::return_output(state.owner_id.as_bytes()); } /// Get bridge prover id for this contract. #[no_mangle] pub extern "C" fn get_bridge_prover() { - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); sdk::return_output(state.bridge_prover_id.as_bytes()); } /// Get chain id for this contract. #[no_mangle] pub extern "C" fn get_chain_id() { - sdk::return_output(&Engine::get_state().chain_id) + sdk::return_output(&Engine::get_state().sdk_unwrap().chain_id) } #[no_mangle] pub extern "C" fn get_upgrade_index() { - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); let index = sdk::read_u64(&bytes_to_key(KeyPrefix::Config, CODE_STAGE_KEY)) - .sdk_expect("ERR_NO_UPGRADE"); + .sdk_expect("ERR_NO_UPGRADE") + .sdk_unwrap(); sdk::return_output(&(index + state.upgrade_delay_blocks).to_le_bytes()) } /// Stage new code for deployment. #[no_mangle] pub extern "C" fn stage_upgrade() { - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); require_owner_only(&state); sdk::read_input_and_store(&bytes_to_key(KeyPrefix::Config, CODE_KEY)); sdk::write_storage( @@ -135,8 +136,10 @@ mod contract { /// Deploy staged upgrade. #[no_mangle] pub extern "C" fn deploy_upgrade() { - let state = Engine::get_state(); - let index = sdk::read_u64(&bytes_to_key(KeyPrefix::Config, CODE_STAGE_KEY)).sdk_unwrap(); + let state = Engine::get_state().sdk_unwrap(); + let index = sdk::read_u64(&bytes_to_key(KeyPrefix::Config, CODE_STAGE_KEY)) + .sdk_expect("ERR_NO_UPGRADE") + .sdk_unwrap(); if sdk::block_index() <= index + state.upgrade_delay_blocks { sdk::panic_utf8(b"ERR_NOT_ALLOWED:TOO_EARLY"); } @@ -151,7 +154,7 @@ mod contract { #[no_mangle] pub extern "C" fn deploy_code() { let input = sdk::read_input(); - let mut engine = Engine::new(predecessor_address()); + let mut engine = Engine::new(predecessor_address()).sdk_unwrap(); Engine::deploy_code_with_input(&mut engine, input) .map(|res| res.try_to_vec().sdk_expect("ERR_SERIALIZE")) .sdk_process(); @@ -161,10 +164,8 @@ mod contract { /// Call method on the EVM contract. #[no_mangle] pub extern "C" fn call() { - // TODO: Borsh input pattern is so common here. It worth writing sdk::read_input_borsh(). - let input = sdk::read_input(); - let args = FunctionCallArgs::try_from_slice(&input).sdk_expect("ERR_ARG_PARSE"); - let mut engine = Engine::new(predecessor_address()); + let args: FunctionCallArgs = sdk::read_input_borsh().sdk_unwrap(); + let mut engine = Engine::new(predecessor_address()).sdk_unwrap(); Engine::call_with_args(&mut engine, args) .map(|res| res.try_to_vec().sdk_expect("ERR_SERIALIZE")) .sdk_process(); @@ -182,7 +183,7 @@ mod contract { let signed_transaction = EthSignedTransaction::decode(&Rlp::new(&input)).sdk_expect("ERR_INVALID_TX"); - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); // Validate the chain ID, if provided inside the signature: if let Some(chain_id) = signed_transaction.chain_id() { @@ -218,7 +219,7 @@ mod contract { #[no_mangle] pub extern "C" fn meta_call() { let input = sdk::read_input(); - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); let domain_separator = crate::meta_parsing::near_erc712_domain(U256::from(state.chain_id)); let meta_call_args = crate::meta_parsing::parse_meta_call( &domain_separator, @@ -247,7 +248,7 @@ mod contract { let input = sdk::read_input(); let dest_address = Address::from_slice(&input); let source_address = predecessor_address(); - let engine = Engine::new(source_address); + let engine = Engine::new(source_address).sdk_unwrap(); engine.increment_nonce(&source_address); @@ -257,14 +258,12 @@ mod contract { #[no_mangle] pub extern "C" fn register_relayer() { - let relayer_address = sdk::read_input(); - // NOTE: Why not `sdk::read_input_arr20();`? - assert_eq!(relayer_address.len(), 20); + let relayer_address = sdk::read_input_arr20().sdk_unwrap(); - let mut engine = Engine::new(predecessor_address()); + let mut engine = Engine::new(predecessor_address()).sdk_unwrap(); engine.register_relayer( sdk::predecessor_account_id().as_slice(), - Address(relayer_address.as_slice().try_into().unwrap()), + Address(relayer_address), ); } @@ -287,38 +286,36 @@ mod contract { #[no_mangle] pub extern "C" fn view() { - let input = sdk::read_input(); - let args = ViewCallArgs::try_from_slice(&input).sdk_expect("ERR_ARG_PARSE"); - let engine = Engine::new(Address::from_slice(&args.sender)); + let args: ViewCallArgs = sdk::read_input_borsh().sdk_unwrap(); + let engine = Engine::new(Address::from_slice(&args.sender)).sdk_unwrap(); let result = Engine::view_with_args(&engine, args); result.sdk_process() } #[no_mangle] pub extern "C" fn get_code() { - let address = sdk::read_input_arr20(); + let address = sdk::read_input_arr20().sdk_unwrap(); let code = Engine::get_code(&Address(address)); sdk::return_output(&code) } #[no_mangle] pub extern "C" fn get_balance() { - let address = sdk::read_input_arr20(); + let address = sdk::read_input_arr20().sdk_unwrap(); let balance = Engine::get_balance(&Address(address)); - sdk::return_output(&u256_to_arr(&balance)) + sdk::return_output(&balance.to_bytes()) } #[no_mangle] pub extern "C" fn get_nonce() { - let address = sdk::read_input_arr20(); + let address = sdk::read_input_arr20().sdk_unwrap(); let nonce = Engine::get_nonce(&Address(address)); sdk::return_output(&u256_to_arr(&nonce)) } #[no_mangle] pub extern "C" fn get_storage_at() { - let input = sdk::read_input(); - let args = GetStorageAtArgs::try_from_slice(&input).sdk_expect("ERR_ARG_PARSE"); + let args: GetStorageAtArgs = sdk::read_input_borsh().sdk_unwrap(); let value = Engine::get_storage(&Address(args.address), &H256(args.key)); sdk::return_output(&value.0) } @@ -330,17 +327,16 @@ mod contract { #[cfg(feature = "evm_bully")] #[no_mangle] pub extern "C" fn begin_chain() { - let mut state = Engine::get_state(); + let mut state = Engine::get_state().sdk_unwrap(); require_owner_only(&state); - let input = sdk::read_input(); - let args = BeginChainArgs::try_from_slice(&input).sdk_expect("ERR_ARG_PARSE"); + let args: BeginChainArgs = sdk::read_input_borsh().sdk_unwrap(); state.chain_id = args.chain_id; Engine::set_state(state); // set genesis block balances for account_balance in args.genesis_alloc { Engine::set_balance( &Address(account_balance.address), - &U256::from(account_balance.balance), + &crate::types::Wei::new(U256::from(account_balance.balance)), ) } // return new chain ID @@ -350,10 +346,9 @@ mod contract { #[cfg(feature = "evm_bully")] #[no_mangle] pub extern "C" fn begin_block() { - let state = Engine::get_state(); + let state = Engine::get_state().sdk_unwrap(); require_owner_only(&state); - let input = sdk::read_input(); - let _args = BeginBlockArgs::try_from_slice(&input).sdk_expect("ERR_ARG_PARSE"); + let _args: BeginBlockArgs = sdk::read_input_borsh().sdk_unwrap(); // TODO: https://github.com/aurora-is-near/aurora-engine/issues/2 } diff --git a/src/meta_parsing.rs b/src/meta_parsing.rs index 219f2302c..6b5dfcbfa 100644 --- a/src/meta_parsing.rs +++ b/src/meta_parsing.rs @@ -5,7 +5,7 @@ use rlp::{Decodable, DecoderError, Rlp}; use crate::parameters::MetaCallArgs; use crate::prelude::{vec, Address, Box, HashMap, String, ToOwned, ToString, Vec, H256, U256}; -use crate::types::{keccak, u256_to_arr, InternalMetaCallArgs, RawU256}; +use crate::types::{keccak, u256_to_arr, InternalMetaCallArgs, RawU256, Wei}; /// Internal errors to propagate up and format in the single place. pub enum ParsingError { @@ -13,6 +13,7 @@ pub enum ParsingError { InvalidMetaTransactionMethodName, InvalidMetaTransactionFunctionArg, InvalidEcRecoverSignature, + ArgsLengthMismatch, } pub type ParsingResult = core::result::Result; @@ -493,10 +494,10 @@ pub fn prepare_meta_call_args( bytes.extend_from_slice(&keccak(types.as_bytes()).as_bytes()); bytes.extend_from_slice(&keccak(account_id).as_bytes()); bytes.extend_from_slice(&u256_to_arr(&input.nonce)); - bytes.extend_from_slice(&u256_to_arr(&input.fee_amount)); + bytes.extend_from_slice(&input.fee_amount.to_bytes()); bytes.extend_from_slice(&encode_address(input.fee_address)); bytes.extend_from_slice(&encode_address(input.contract_address)); - bytes.extend_from_slice(&u256_to_arr(&input.value)); + bytes.extend_from_slice(&input.value.to_bytes()); let methods = MethodAndTypes::parse(&method_def)?; let method_sig = method_signature(&methods); @@ -505,9 +506,11 @@ pub fn prepare_meta_call_args( let mut arg_bytes = Vec::new(); arg_bytes.extend_from_slice(&keccak(arguments.as_bytes()).as_bytes()); let args_decoded: Vec = rlp_decode(&input.input)?; + if methods.method.args.len() != args_decoded.len() { + return Err(ParsingError::ArgsLengthMismatch); + } for (i, arg) in args_decoded.iter().enumerate() { arg_bytes.extend_from_slice(&eip_712_hash_argument( - // TODO: Check that method.args.len() == args_decoded.len(). Otherwise it may panic here. &methods.method.args[i].t, arg, &methods.types, @@ -544,10 +547,10 @@ pub fn parse_meta_call( let meta_tx = MetaCallArgs::try_from_slice(&args).map_err(|_| ParsingError::ArgumentParseError)?; let nonce = U256::from(meta_tx.nonce); - let fee_amount = U256::from(meta_tx.fee_amount); + let fee_amount = Wei::new(U256::from(meta_tx.fee_amount)); let fee_address = Address::from(meta_tx.fee_address); let contract_address = Address::from(meta_tx.contract_address); - let value = U256::from(meta_tx.value); + let value = Wei::new(U256::from(meta_tx.value)); let mut result = InternalMetaCallArgs { sender: Address::zero(), diff --git a/src/precompiles/mod.rs b/src/precompiles/mod.rs index 2713a6936..33667e17e 100644 --- a/src/precompiles/mod.rs +++ b/src/precompiles/mod.rs @@ -3,6 +3,7 @@ mod bn128; mod hash; mod identity; mod modexp; +mod native; mod secp256k1; use crate::precompiles::blake2::Blake2F; @@ -10,11 +11,24 @@ use crate::precompiles::bn128::{BN128Add, BN128Mul, BN128Pair}; use crate::precompiles::hash::{RIPEMD160, SHA256}; use crate::precompiles::identity::Identity; use crate::precompiles::modexp::ModExp; +use crate::precompiles::native::{ExitToEthereum, ExitToNear}; pub(crate) use crate::precompiles::secp256k1::ecrecover; use crate::precompiles::secp256k1::ECRecover; use crate::prelude::{Address, Vec}; use evm::{Context, ExitError, ExitSucceed}; +/// Exit to Ethereum precompile address (truncated to 8 bytes) +/// +/// Address: `0xb0bd02f6a392af548bdf1cfaee5dfa0eefcc8eab` +/// This address is computed as: `&keccak("exitToEthereum")[12..]` +const EXIT_TO_ETHEREUM_ID: u64 = 17176159495920586411; + +/// Exit to NEAR precompile address (truncated to 8 bytes) +/// +/// Address: `0xe9217bc70b7ed1f598ddd3199e80b093fa71124f` +/// This address is computed as: `&keccak("exitToNear")[12..]` +const EXIT_TO_NEAR_ID: u64 = 11421322804619973199; + /// A precompile operation result. type PrecompileResult = Result<(ExitSucceed, Vec, u64), ExitError>; @@ -78,7 +92,8 @@ pub fn homestead_precompiles( 1 => Some(ECRecover::run(input, target_gas, context)), 2 => Some(SHA256::run(input, target_gas, context)), 3 => Some(RIPEMD160::run(input, target_gas, context)), - // 4 => Some(identity::identity(input, target_gas)), + EXIT_TO_NEAR_ID => Some(ExitToNear::run(input, target_gas, context)), + EXIT_TO_ETHEREUM_ID => Some(ExitToEthereum::run(input, target_gas, context)), _ => None, } } @@ -105,6 +120,8 @@ pub fn byzantium_precompiles( 6 => Some(BN128Add::::run(input, target_gas, context)), 7 => Some(BN128Mul::::run(input, target_gas, context)), 8 => Some(BN128Pair::::run(input, target_gas, context)), + EXIT_TO_NEAR_ID => Some(ExitToNear::run(input, target_gas, context)), + EXIT_TO_ETHEREUM_ID => Some(ExitToEthereum::run(input, target_gas, context)), _ => None, } } @@ -132,7 +149,8 @@ pub fn istanbul_precompiles( 7 => Some(BN128Mul::::run(input, target_gas, context)), 8 => Some(BN128Pair::::run(input, target_gas, context)), 9 => Some(Blake2F::run(input, target_gas, context)), - // Not supported. + EXIT_TO_NEAR_ID => Some(ExitToNear::run(input, target_gas, context)), + EXIT_TO_ETHEREUM_ID => Some(ExitToEthereum::run(input, target_gas, context)), _ => None, } } @@ -160,7 +178,10 @@ pub fn berlin_precompiles( 7 => Some(BN128Mul::::run(input, target_gas, context)), 8 => Some(BN128Pair::::run(input, target_gas, context)), 9 => Some(Blake2F::run(input, target_gas, context)), - // Not supported. + #[cfg(feature = "contract")] + EXIT_TO_NEAR_ID => Some(ExitToNear::run(input, target_gas, context)), + #[cfg(feature = "contract")] + EXIT_TO_ETHEREUM_ID => Some(ExitToEthereum::run(input, target_gas, context)), _ => None, } } diff --git a/src/precompiles/native.rs b/src/precompiles/native.rs new file mode 100644 index 000000000..df2bbd18b --- /dev/null +++ b/src/precompiles/native.rs @@ -0,0 +1,263 @@ +use evm::{Context, ExitError, ExitSucceed}; + +use super::{Precompile, PrecompileResult}; +use crate::prelude::{Cow, String, ToString, Vec, U256}; +use crate::types::AccountId; + +mod costs { + use crate::types::Gas; + + // TODO(#51): Determine the correct amount of gas + pub(super) const EXIT_TO_NEAR_GAS: Gas = 0; + + // TODO(#51): Determine the correct amount of gas + pub(super) const EXIT_TO_ETHEREUM_GAS: Gas = 0; + + // TODO(#51): Determine the correct amount of gas + pub(super) const FT_TRANSFER_GAS: Gas = 100_000_000_000_000; + + // TODO(#51): Determine the correct amount of gas + pub(super) const WITHDRAWAL_GAS: Gas = 100_000_000_000_000; +} + +/// Get the current nep141 token associated with the current erc20 token. +/// This will fail is none is associated. +fn get_nep141_from_erc20(_erc20_token: &[u8]) -> AccountId { + // TODO(#51): Already implemented + "".to_string() +} + +/// The minimum length of a valid account ID. +const MIN_ACCOUNT_ID_LEN: u64 = 2; +/// The maximum length of a valid account ID. +const MAX_ACCOUNT_ID_LEN: u64 = 64; + +/// Returns `true` if the given account ID is valid and `false` otherwise. +/// +/// Taken from near-sdk-rs: +/// (https://github.com/near/near-sdk-rs/blob/42f62384c3acd024829501ee86e480917da03896/near-sdk/src/environment/env.rs#L816-L843) +pub fn is_valid_account_id(account_id: &[u8]) -> bool { + if (account_id.len() as u64) < MIN_ACCOUNT_ID_LEN + || (account_id.len() as u64) > MAX_ACCOUNT_ID_LEN + { + return false; + } + + // NOTE: We don't want to use Regex here, because it requires extra time to compile it. + // The valid account ID regex is /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/ + // Instead the implementation is based on the previous character checks. + + // We can safely assume that last char was a separator. + let mut last_char_is_separator = true; + + for c in account_id { + let current_char_is_separator = match *c { + b'a'..=b'z' | b'0'..=b'9' => false, + b'-' | b'_' | b'.' => true, + _ => return false, + }; + if current_char_is_separator && last_char_is_separator { + return false; + } + last_char_is_separator = current_char_is_separator; + } + // The account can't end as separator. + !last_char_is_separator +} + +pub struct ExitToNear; //TransferEthToNear + +impl Precompile for ExitToNear { + fn required_gas(_input: &[u8]) -> Result { + Ok(costs::EXIT_TO_NEAR_GAS) + } + + #[cfg(not(feature = "contract"))] + fn run(input: &[u8], target_gas: u64, context: &Context) -> PrecompileResult { + if Self::required_gas(input)? > target_gas { + return Err(ExitError::OutOfGas); + } + + Ok((ExitSucceed::Returned, Vec::new(), 0)) + } + + #[cfg(feature = "contract")] + fn run(input: &[u8], target_gas: u64, context: &Context) -> PrecompileResult { + if Self::required_gas(input)? > target_gas { + return Err(ExitError::OutOfGas); + } + + let (nep141_address, args) = if context.apparent_value != U256::from(0) { + // ETH transfer + // + // Input slice format: + // recipient_account_id (bytes) - the NEAR recipient account which will receive NEP-141 ETH tokens + + if is_valid_account_id(input) { + ( + String::from_utf8(crate::sdk::current_account_id()).unwrap(), + crate::prelude::format!( + r#"{{"receiver_id": "{}", "amount": "{}", "memo": null}}"#, + String::from_utf8(input.to_vec()).unwrap(), + context.apparent_value.as_u128() + ), + ) + } else { + return Err(ExitError::Other(Cow::from( + "ERR_INVALID_RECEIVER_ACCOUNT_ID", + ))); + } + } else { + // ERC20 transfer + // + // This precompile branch is expected to be called from the ERC20 burn function\ + // + // Input slice format: + // amount (U256 le bytes) - the amount that was burned + // recipient_account_id (bytes) - the NEAR recipient account which will receive NEP-141 tokens + + let nep141_address = get_nep141_from_erc20(context.caller.as_bytes()); + + let mut input_mut = input; + let amount = U256::from_big_endian(&input_mut[..32]).as_u128(); + input_mut = &input_mut[32..]; + + if is_valid_account_id(input_mut) { + let receiver_account_id: AccountId = String::from_utf8(input_mut.to_vec()).unwrap(); + ( + nep141_address, + crate::prelude::format!( + r#"{{"receiver_id": "{}", "amount": "{}", "memo": null}}"#, + receiver_account_id, + amount + ), + ) + } else { + return Err(ExitError::Other(Cow::from( + "ERR_INVALID_RECEIVER_ACCOUNT_ID", + ))); + } + }; + + let promise0 = crate::sdk::promise_create( + nep141_address, + b"ft_transfer", + args.as_bytes(), + 1, + costs::FT_TRANSFER_GAS, + ); + + crate::sdk::promise_return(promise0); + + Ok((ExitSucceed::Returned, Vec::new(), 0)) + } +} + +pub struct ExitToEthereum; + +impl Precompile for ExitToEthereum { + fn required_gas(_input: &[u8]) -> Result { + Ok(costs::EXIT_TO_ETHEREUM_GAS) + } + + #[cfg(not(feature = "contract"))] + fn run(input: &[u8], target_gas: u64, context: &Context) -> PrecompileResult { + if Self::required_gas(input)? > target_gas { + return Err(ExitError::OutOfGas); + } + + Ok((ExitSucceed::Returned, Vec::new(), 0)) + } + + #[cfg(feature = "contract")] + fn run(input: &[u8], target_gas: u64, context: &Context) -> PrecompileResult { + if Self::required_gas(input)? > target_gas { + return Err(ExitError::OutOfGas); + } + + let (nep141_address, serialized_args) = if context.apparent_value != U256::from(0) { + // ETH transfer + // + // Input slice format: + // eth_recipient (20 bytes) - the address of recipient which will receive ETH on Ethereum + + let eth_recipient: String = hex::encode(input); + + if eth_recipient.len() == 20 { + ( + String::from_utf8(crate::sdk::current_account_id()).unwrap(), + crate::prelude::format!( + r#"{{"amount": "{}", "recipient": "{}"}}"#, + context.apparent_value.as_u128(), + eth_recipient + ), + ) + } else { + return Err(ExitError::Other(Cow::from("ERR_INVALID_RECIPIENT_ADDRESS"))); + } + } else { + // ERC-20 transfer + // + // This precompile branch is expected to be called from the ERC20 withdraw function + // (or burn function with some flag provided that this is expected to be withdrawn) + // + // Input slice format: + // amount (U256 le bytes) - the amount that was burned + // eth_recipient (20 bytes) - the address of recipient which will receive ETH on Ethereum + + let nep141_address = get_nep141_from_erc20(context.caller.as_bytes()); + + let mut input_mut = input; + + let amount = U256::from_big_endian(&input_mut[..32]).as_u128(); + input_mut = &input_mut[32..]; + + if input_mut.len() == 20 { + // Parse ethereum address in hex + let eth_recipient: String = hex::encode(input_mut.to_vec()); + + ( + nep141_address, + crate::prelude::format!( + r#"{{"amount": "{}", "recipient": "{}"}}"#, + amount, + eth_recipient + ), + ) + } else { + return Err(ExitError::Other(Cow::from("ERR_INVALID_RECIPIENT_ADDRESS"))); + } + }; + + let promise0 = crate::sdk::promise_create( + nep141_address, + b"withdraw", + serialized_args.as_bytes(), + 1, + costs::WITHDRAWAL_GAS, + ); + + crate::sdk::promise_return(promise0); + + Ok((ExitSucceed::Returned, Vec::new(), 0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::precompiles::{EXIT_TO_ETHEREUM_ID, EXIT_TO_NEAR_ID}; + use crate::types::near_account_to_evm_address; + + #[test] + fn test_precompile_id() { + assert_eq!( + EXIT_TO_ETHEREUM_ID, + near_account_to_evm_address("exitToEthereum".as_bytes()).to_low_u64_be() + ); + assert_eq!( + EXIT_TO_NEAR_ID, + near_account_to_evm_address("exitToNear".as_bytes()).to_low_u64_be() + ); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index a81da0d5d..86dd659c9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,18 +4,36 @@ pub use alloc::{ borrow::{Cow, Cow::*}, boxed::Box, collections::BTreeMap as HashMap, - fmt, + fmt, format, string::String, string::ToString, vec, vec::Vec, }; #[cfg(not(feature = "std"))] -pub use core::{convert::TryInto, marker::PhantomData, mem}; +pub use core::{ + convert::TryInto, + marker::PhantomData, + mem, + ops::{Add, Sub}, +}; #[cfg(feature = "std")] pub use std::{ - borrow::Cow::Borrowed, borrow::ToOwned, boxed::Box, collections::HashMap, convert::TryInto, - error::Error, fmt, marker::PhantomData, mem, string::String, string::ToString, vec, vec::Vec, + borrow::Cow, + borrow::Cow::Borrowed, + borrow::ToOwned, + boxed::Box, + collections::HashMap, + convert::TryInto, + error::Error, + fmt, format, + marker::PhantomData, + mem, + ops::{Add, Sub}, + string::String, + string::ToString, + vec, + vec::Vec, }; pub use primitive_types::{H160, H256, U256}; diff --git a/src/sdk.rs b/src/sdk.rs index 467f7482f..fc4faa4a9 100644 --- a/src/sdk.rs +++ b/src/sdk.rs @@ -2,6 +2,9 @@ use crate::prelude::{vec, String, Vec, H256}; use crate::types::STORAGE_PRICE_PER_BYTE; use borsh::{BorshDeserialize, BorshSerialize}; +const READ_STORAGE_REGISTER_ID: u64 = 0; +const INPUT_REGISTER_ID: u64 = 0; + mod exports { #[allow(unused)] @@ -15,8 +18,8 @@ mod exports { // # Context API # // ############### pub(crate) fn current_account_id(register_id: u64); - fn signer_account_id(register_id: u64); - fn signer_account_pk(register_id: u64); + pub(crate) fn signer_account_id(register_id: u64); + pub(crate) fn signer_account_pk(register_id: u64); pub(crate) fn predecessor_account_id(register_id: u64); pub(crate) fn input(register_id: u64); // TODO #1903 fn block_height() -> u64; @@ -156,24 +159,30 @@ mod exports { } } -#[allow(dead_code)] pub fn read_input() -> Vec { unsafe { - exports::input(0); - let bytes: Vec = vec![0; exports::register_len(0) as usize]; - exports::read_register(0, bytes.as_ptr() as *const u64 as u64); + exports::input(INPUT_REGISTER_ID); + let bytes: Vec = vec![0; exports::register_len(INPUT_REGISTER_ID) as usize]; + exports::read_register(INPUT_REGISTER_ID, bytes.as_ptr() as *const u64 as u64); bytes } } -#[allow(dead_code)] -pub fn read_input_arr20() -> [u8; 20] { +pub(crate) fn read_input_borsh() -> Result { + let bytes = read_input(); + T::try_from_slice(&bytes).map_err(|_| ArgParseErr) +} + +pub(crate) fn read_input_arr20() -> Result<[u8; 20], IncorrectInputLength> { unsafe { - exports::input(0); - let bytes = [0u8; 20]; - // TODO: Is it fine to not check the length of the input register here? - exports::read_register(0, bytes.as_ptr() as *const u64 as u64); - bytes + exports::input(INPUT_REGISTER_ID); + if exports::register_len(INPUT_REGISTER_ID) == 20 { + let bytes = [0u8; 20]; + exports::read_register(INPUT_REGISTER_ID, bytes.as_ptr() as *const u64 as u64); + Ok(bytes) + } else { + Err(IncorrectInputLength) + } } } @@ -195,11 +204,25 @@ pub fn return_output(value: &[u8]) { #[allow(dead_code)] pub fn read_storage(key: &[u8]) -> Option> { + read_storage_len(key).map(|value_size| unsafe { + let bytes = vec![0u8; value_size]; + exports::read_register( + READ_STORAGE_REGISTER_ID, + bytes.as_ptr() as *const u64 as u64, + ); + bytes + }) +} + +pub fn read_storage_len(key: &[u8]) -> Option { unsafe { - if exports::storage_read(key.len() as u64, key.as_ptr() as u64, 0) == 1 { - let bytes: Vec = vec![0u8; exports::register_len(0) as usize]; - exports::read_register(0, bytes.as_ptr() as *const u64 as u64); - Some(bytes) + if exports::storage_read( + key.len() as u64, + key.as_ptr() as u64, + READ_STORAGE_REGISTER_ID, + ) == 1 + { + Some(exports::register_len(READ_STORAGE_REGISTER_ID) as usize) } else { None } @@ -207,17 +230,16 @@ pub fn read_storage(key: &[u8]) -> Option> { } /// Read u64 from storage at given key. -pub fn read_u64(key: &[u8]) -> Option { - unsafe { - if exports::storage_read(key.len() as u64, key.as_ptr() as u64, 0) == 1 { +pub(crate) fn read_u64(key: &[u8]) -> Option> { + read_storage_len(key).map(|value_size| unsafe { + if value_size == 8 { let result = [0u8; 8]; - // TODO: Are you sure the register length is correct? - exports::read_register(0, result.as_ptr() as _); - Some(u64::from_le_bytes(result)) + exports::read_register(READ_STORAGE_REGISTER_ID, result.as_ptr() as _); + Ok(u64::from_le_bytes(result)) } else { - None + Err(InvalidU64) } - } + }) } #[allow(dead_code)] @@ -484,3 +506,24 @@ pub fn promise_batch_create(account_id: String) -> u64 { pub fn storage_has_key(key: &[u8]) -> bool { unsafe { exports::storage_has_key(key.len() as u64, key.as_ptr() as u64) == 1 } } + +pub(crate) struct IncorrectInputLength; +impl AsRef<[u8]> for IncorrectInputLength { + fn as_ref(&self) -> &[u8] { + b"ERR_INCORRECT_INPUT_LENGTH" + } +} + +pub(crate) struct ArgParseErr; +impl AsRef<[u8]> for ArgParseErr { + fn as_ref(&self) -> &[u8] { + b"ERR_ARG_PARSE" + } +} + +pub(crate) struct InvalidU64; +impl AsRef<[u8]> for InvalidU64 { + fn as_ref(&self) -> &[u8] { + b"ERR_NOT_U64" + } +} diff --git a/src/test_utils/mod.rs b/src/test_utils/mod.rs index 6e2809350..9cd3c2a09 100644 --- a/src/test_utils/mod.rs +++ b/src/test_utils/mod.rs @@ -114,11 +114,11 @@ impl AuroraRunner { ) } - pub fn create_address(&mut self, address: Address, init_balance: U256, init_nonce: U256) { + pub fn create_address(&mut self, address: Address, init_balance: types::Wei, init_nonce: U256) { let trie = &mut self.ext.fake_trie; let balance_key = storage::address_to_key(storage::KeyPrefix::Balance, &address); - let balance_value = types::u256_to_arr(&init_balance); + let balance_value = init_balance.to_bytes(); let nonce_key = storage::address_to_key(storage::KeyPrefix::Nonce, &address); let nonce_value = types::u256_to_arr(&init_nonce); @@ -170,8 +170,8 @@ impl AuroraRunner { } } - pub fn get_balance(&self, address: Address) -> U256 { - self.getter_method_call("get_balance", address) + pub fn get_balance(&self, address: Address) -> types::Wei { + types::Wei::new(self.getter_method_call("get_balance", address)) } pub fn get_nonce(&self, address: Address) -> U256 { @@ -262,7 +262,7 @@ pub(crate) fn deploy_evm() -> AuroraRunner { pub(crate) fn create_eth_transaction( to: Option
, - value: U256, + value: types::Wei, data: Vec, chain_id: Option, secret_key: &SecretKey, @@ -322,7 +322,7 @@ pub(crate) fn parse_eth_gas(output: &VMOutcome) -> u64 { pub(crate) fn validate_address_balance_and_nonce( runner: &AuroraRunner, address: Address, - expected_balance: U256, + expected_balance: types::Wei, expected_nonce: U256, ) { assert_eq!(runner.get_balance(address), expected_balance, "balance"); diff --git a/src/tests/erc20.rs b/src/tests/erc20.rs index 4790bb3a8..54769bde8 100644 --- a/src/tests/erc20.rs +++ b/src/tests/erc20.rs @@ -3,6 +3,7 @@ use crate::test_utils::{ self, erc20::{ERC20Constructor, ERC20}, }; +use crate::types::Wei; use bstr::ByteSlice; use secp256k1::SecretKey; @@ -211,7 +212,11 @@ fn initialize_erc20() -> (test_utils::AuroraRunner, SecretKey, Address, ERC20) { let mut rng = rand::thread_rng(); let source_account = SecretKey::random(&mut rng); let source_address = test_utils::address_from_secret_key(&source_account); - runner.create_address(source_address, INITIAL_BALANCE.into(), INITIAL_NONCE.into()); + runner.create_address( + source_address, + Wei::new_u64(INITIAL_BALANCE), + INITIAL_NONCE.into(), + ); let dest_address = test_utils::address_from_secret_key(&SecretKey::random(&mut rng)); let constructor = ERC20Constructor::load(); diff --git a/src/tests/sanity.rs b/src/tests/sanity.rs index 067119a5b..b89a81887 100644 --- a/src/tests/sanity.rs +++ b/src/tests/sanity.rs @@ -1,11 +1,12 @@ use crate::prelude::Address; use crate::test_utils; use crate::transaction::EthTransaction; +use crate::types::Wei; use secp256k1::SecretKey; -const INITIAL_BALANCE: u64 = 1000; +const INITIAL_BALANCE: Wei = Wei::new_u64(1000); const INITIAL_NONCE: u64 = 0; -const TRANSFER_AMOUNT: u64 = 123; +const TRANSFER_AMOUNT: Wei = Wei::new_u64(123); /// Tests we can transfer Eth from one account to another and that the balances are correctly /// updated. @@ -28,10 +29,10 @@ fn test_eth_transfer_success() { test_utils::validate_address_balance_and_nonce( &runner, source_address, - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); - test_utils::validate_address_balance_and_nonce(&runner, dest_address, 0.into(), 0.into()); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); // perform transfer let (_, maybe_err) = runner.call(test_utils::SUBMIT, calling_account_id, input); @@ -41,13 +42,13 @@ fn test_eth_transfer_success() { test_utils::validate_address_balance_and_nonce( &runner, source_address, - (INITIAL_BALANCE - TRANSFER_AMOUNT).into(), + INITIAL_BALANCE - TRANSFER_AMOUNT, (INITIAL_NONCE + 1).into(), ); test_utils::validate_address_balance_and_nonce( &runner, dest_address, - TRANSFER_AMOUNT.into(), + TRANSFER_AMOUNT, 0.into(), ); } @@ -59,7 +60,7 @@ fn test_eth_transfer_insufficient_balance() { let source_address = test_utils::address_from_secret_key(&source_account); let transaction = test_utils::create_eth_transaction( Some(dest_address), - (2 * INITIAL_BALANCE).into(), // trying to transfer more than we have + INITIAL_BALANCE + INITIAL_BALANCE, // trying to transfer more than we have vec![], Some(runner.chain_id), &source_account, @@ -71,10 +72,10 @@ fn test_eth_transfer_insufficient_balance() { test_utils::validate_address_balance_and_nonce( &runner, source_address, - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); - test_utils::validate_address_balance_and_nonce(&runner, dest_address, 0.into(), 0.into()); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); // attempt transfer let (_, maybe_err) = runner.call(test_utils::SUBMIT, calling_account_id, input); @@ -85,11 +86,11 @@ fn test_eth_transfer_insufficient_balance() { test_utils::validate_address_balance_and_nonce( &runner, source_address, - INITIAL_BALANCE.into(), + INITIAL_BALANCE, // the nonce is still incremented even though the transfer failed (INITIAL_NONCE + 1).into(), ); - test_utils::validate_address_balance_and_nonce(&runner, dest_address, 0.into(), 0.into()); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); } /// Tests the case where the nonce on the transaction does not match the address @@ -114,10 +115,10 @@ fn test_eth_transfer_incorrect_nonce() { test_utils::validate_address_balance_and_nonce( &runner, source_address, - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); - test_utils::validate_address_balance_and_nonce(&runner, dest_address, 0.into(), 0.into()); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); // attempt transfer let (_, maybe_err) = runner.call(test_utils::SUBMIT, calling_account_id, input); @@ -128,10 +129,10 @@ fn test_eth_transfer_incorrect_nonce() { test_utils::validate_address_balance_and_nonce( &runner, source_address, - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); - test_utils::validate_address_balance_and_nonce(&runner, dest_address, 0.into(), 0.into()); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); } fn initialize_transfer() -> (test_utils::AuroraRunner, SecretKey, Address) { @@ -140,7 +141,7 @@ fn initialize_transfer() -> (test_utils::AuroraRunner, SecretKey, Address) { let mut rng = rand::thread_rng(); let source_account = SecretKey::random(&mut rng); let source_address = test_utils::address_from_secret_key(&source_account); - runner.create_address(source_address, INITIAL_BALANCE.into(), INITIAL_NONCE.into()); + runner.create_address(source_address, INITIAL_BALANCE, INITIAL_NONCE.into()); let dest_address = test_utils::address_from_secret_key(&SecretKey::random(&mut rng)); (runner, source_account, dest_address) diff --git a/src/tests/standard_precompiles.rs b/src/tests/standard_precompiles.rs index 15e986182..bf2ca483a 100644 --- a/src/tests/standard_precompiles.rs +++ b/src/tests/standard_precompiles.rs @@ -2,9 +2,10 @@ use crate::test_utils::{ self, standard_precompiles::{PrecompilesConstructor, PrecompilesContract}, }; +use crate::types::Wei; use secp256k1::SecretKey; -const INITIAL_BALANCE: u64 = 1000; +const INITIAL_BALANCE: Wei = Wei::new_u64(1000); const INITIAL_NONCE: u64 = 0; #[test] @@ -14,7 +15,7 @@ fn standard_precompiles() { let source_account = SecretKey::random(&mut rng); runner.create_address( test_utils::address_from_secret_key(&source_account), - INITIAL_BALANCE.into(), + INITIAL_BALANCE, INITIAL_NONCE.into(), ); diff --git a/src/transaction.rs b/src/transaction.rs index 91152d292..fe1be41ba 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,4 +1,5 @@ use crate::prelude::{Address, Vec, U256}; +use crate::types::Wei; use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; #[derive(Debug, Eq, PartialEq)] @@ -12,7 +13,7 @@ pub struct EthTransaction { /// The receiving address (`None` for the zero address) pub to: Option
, /// The amount of ETH to transfer - pub value: U256, + pub value: Wei, /// Arbitrary binary data for a contract call invocation pub data: Vec, } @@ -27,7 +28,7 @@ impl EthTransaction { None => s.append(&""), Some(address) => s.append(address), }; - s.append(&self.value); + s.append(&self.value.raw()); s.append(&self.data); if let Some(chain_id) = chain_id { s.append(&chain_id); @@ -87,7 +88,7 @@ impl Encodable for EthSignedTransaction { None => s.append(&""), Some(address) => s.append(address), }; - s.append(&self.transaction.value); + s.append(&self.transaction.value.raw()); s.append(&self.transaction.data); s.append(&self.v); s.append(&self.r); @@ -120,7 +121,7 @@ impl Decodable for EthSignedTransaction { } } }; - let value = rlp.val_at(4)?; + let value = Wei::new(rlp.val_at(4)?); let data = rlp.val_at(5)?; let v = rlp.val_at(6)?; let r = rlp.val_at(7)?; @@ -181,7 +182,7 @@ mod tests { to: Some(address_from_arr( &hex::decode("F0109fC8DF283027b6285cc889F5aA624EaC1F55").unwrap() )), - value: U256::from(1000000000), + value: Wei::new_u64(1000000000), data: vec![], } ); diff --git a/src/types.rs b/src/types.rs index 36e030ee0..4a4a84700 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ -use crate::prelude::{Address, String, Vec, H256, U256}; +use crate::prelude::{self, Address, String, Vec, H256, U256}; +use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(not(feature = "contract"))] use sha3::{Digest, Keccak256}; @@ -10,6 +11,65 @@ pub type AccountId = String; pub type RawAddress = [u8; 20]; pub type RawU256 = [u8; 32]; // Little-endian large integer type. pub type RawH256 = [u8; 32]; // Unformatted binary data of fixed length. +pub type Gas = u64; +pub type Balance = u128; + +/// Newtype to distinguish balances (denominated in Wei) from other U256 types. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)] +pub struct Wei(U256); +impl Wei { + const ETH_TO_WEI: U256 = U256([1_000_000_000_000_000_000, 0, 0, 0]); + + pub const fn zero() -> Self { + Self(U256([0, 0, 0, 0])) + } + + pub fn new(amount: U256) -> Self { + Self(amount) + } + + // Purposely not implementing `From` because I want the call site to always + // say `Wei::`. If `From` is implemented then the caller might write + // `amount.into()` without thinking too hard about the units. Explicitly writing + // `Wei` reminds the developer to think about whether the amount they enter is really + // in units of `Wei` or not. + pub const fn new_u64(amount: u64) -> Self { + Self(U256([amount, 0, 0, 0])) + } + + pub fn from_eth(amount: U256) -> Option { + amount.checked_mul(Self::ETH_TO_WEI).map(Self) + } + + pub fn to_bytes(&self) -> [u8; 32] { + u256_to_arr(&self.0) + } + + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } + + pub fn raw(self) -> U256 { + self.0 + } +} +impl prelude::Sub for Wei { + type Output = Self; + + fn sub(self, other: Self) -> Self::Output { + Self(self.0 - other.0) + } +} +impl prelude::Add for Wei { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +#[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 @@ -18,14 +78,13 @@ pub const STORAGE_PRICE_PER_BYTE: u128 = 10_000_000_000_000_000_000; // 1e19yN, pub struct InternalMetaCallArgs { pub sender: Address, pub nonce: U256, - pub fee_amount: U256, + pub fee_amount: Wei, pub fee_address: Address, pub contract_address: Address, - pub value: U256, + pub value: Wei, pub input: Vec, } -#[allow(dead_code)] pub fn u256_to_arr(value: &U256) -> [u8; 32] { let mut result = [0u8; 32]; value.to_big_endian(&mut result); @@ -72,4 +131,17 @@ mod tests { "0001ff10".to_string() ); } + + #[test] + fn test_wei_from_u64() { + let x: u64 = rand::random(); + assert_eq!(Wei::new_u64(x).raw().as_u64(), x); + } + + #[test] + fn test_wei_from_eth() { + let eth_amount: u64 = rand::random(); + 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))); + } } diff --git a/tests/test_meta_parsing.rs b/tests/test_meta_parsing.rs index 5a518b323..0552badc3 100644 --- a/tests/test_meta_parsing.rs +++ b/tests/test_meta_parsing.rs @@ -5,16 +5,16 @@ use near_crypto::{InMemorySigner, KeyType, PublicKey, Signature, Signer}; use aurora_engine::meta_parsing::{near_erc712_domain, parse_meta_call, prepare_meta_call_args}; use aurora_engine::parameters::MetaCallArgs; use aurora_engine::prelude::{Address, U256}; -use aurora_engine::types::{keccak, u256_to_arr, InternalMetaCallArgs}; +use aurora_engine::types::{keccak, u256_to_arr, InternalMetaCallArgs, Wei}; pub fn encode_meta_call_function_args( signer: &dyn Signer, chain_id: u64, nonce: U256, - fee_amount: U256, + fee_amount: Wei, fee_address: Address, contract_address: Address, - value: U256, + value: Wei, method_def: &str, args: Vec, ) -> Vec { @@ -47,10 +47,10 @@ pub fn encode_meta_call_function_args( // Add 27 to align eth-sig-util signature format v: 27, nonce: u256_to_arr(&nonce), - fee_amount: u256_to_arr(&fee_amount), + fee_amount: fee_amount.to_bytes(), fee_address: fee_address.0, contract_address: contract_address.0, - value: u256_to_arr(&value), + value: value.to_bytes(), method_def: method_def.to_string(), args, } @@ -84,10 +84,10 @@ fn test_meta_parsing() { &signer, chain_id, U256::from(14), - U256::from(6), + Wei::new_u64(6), Address::from_slice(&[0u8; 20]), signer_addr.clone(), - U256::from(0), + Wei::zero(), "adopt(uint256 petId)", // RLP encode of ["0x09"] hex::decode("c109").unwrap(), @@ -104,10 +104,10 @@ fn test_meta_parsing() { &signer, chain_id, U256::from(14), - U256::from(6), + Wei::new_u64(6), Address::from_slice(&[0u8; 20]), signer_addr.clone(), - U256::from(0), + Wei::zero(), "adopt(uint256 petId,PetObj petObject)PetObj(string petName,address owner)", // RLP encode of ["0x09", ["0x436170734C6F636B", "0x0123456789012345678901234567890123456789"]] hex::decode("e009de88436170734c6f636b940123456789012345678901234567890123456789").unwrap(),