diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index fc6d9cc4e..772cd3a72 100644 --- a/engine-precompiles/src/lib.rs +++ b/engine-precompiles/src/lib.rs @@ -15,6 +15,7 @@ pub mod prepaid_gas; pub mod promise_result; pub mod random; pub mod secp256k1; +pub mod set_gas_token; mod utils; pub mod xcc; @@ -111,8 +112,8 @@ impl HardFork for Istanbul {} impl HardFork for Berlin {} pub struct Precompiles<'a, I, E, H> { - pub all_precompiles: prelude::BTreeMap>, - pub paused_precompiles: prelude::BTreeSet
, + pub all_precompiles: BTreeMap>, + pub paused_precompiles: BTreeSet
, } impl<'a, I, E, H> Precompiles<'a, I, E, H> { @@ -149,7 +150,7 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> executor::stack::Preco Some(result.and_then(|output| post_process(output, handle))) } - fn is_precompile(&self, address: prelude::H160) -> bool { + fn is_precompile(&self, address: H160) -> bool { self.all_precompiles.contains_key(&Address::new(address)) } } @@ -270,7 +271,7 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, RandomSeed::ADDRESS, CurrentAccount::ADDRESS, ]; - let fun: prelude::Vec> = vec![ + let fun: Vec> = vec![ Box::new(ECRecover), Box::new(SHA256), Box::new(RIPEMD160), @@ -386,10 +387,10 @@ pub enum AllPrecompiles<'a, I, E, H> { /// fn for making an address by concatenating the bytes from two given numbers, /// Note that 32 + 128 = 160 = 20 bytes (the length of an address). This function is used /// as a convenience for specifying the addresses of the various precompiles. -pub const fn make_address(x: u32, y: u128) -> prelude::types::Address { +pub const fn make_address(x: u32, y: u128) -> Address { let x_bytes = x.to_be_bytes(); let y_bytes = y.to_be_bytes(); - prelude::types::Address::new(H160([ + Address::new(H160([ x_bytes[0], x_bytes[1], x_bytes[2], @@ -413,10 +414,10 @@ pub const fn make_address(x: u32, y: u128) -> prelude::types::Address { ])) } -const fn make_h256(x: u128, y: u128) -> prelude::H256 { +const fn make_h256(x: u128, y: u128) -> H256 { let x_bytes = x.to_be_bytes(); let y_bytes = y.to_be_bytes(); - prelude::H256([ + H256([ x_bytes[0], x_bytes[1], x_bytes[2], diff --git a/engine-precompiles/src/set_gas_token.rs b/engine-precompiles/src/set_gas_token.rs new file mode 100644 index 000000000..3dbcd0ae3 --- /dev/null +++ b/engine-precompiles/src/set_gas_token.rs @@ -0,0 +1,157 @@ +use crate::set_gas_token::events::SetGasTokenLog; +use crate::{EvmPrecompileResult, Precompile, PrecompileOutput}; +use aurora_engine_types::types::{Address, EthGas}; +use evm::backend::Log; +use evm::{Context, ExitError}; +use std::borrow::Cow; + +pub use consts::SET_GAS_TOKEN_ADDRESS; + +mod costs { + use crate::prelude::types::EthGas; + + // TODO: gas costs, could be calculated returning logs of NEAR gas used prior and after. + // Should check if the gas check adds gas itself as well..? + pub(super) const SET_GAS_TOKEN_GAS: EthGas = EthGas::new(0); +} + +pub mod consts { + use aurora_engine_types::types::Address; + + /// Change gas token precompile address. + /// + /// Address: `0x076dae45c8e16a92258252fe04dedd97f1ea93d6` + /// + /// This address is computed as: `keccak("setGasToken")[12..]` + pub const SET_GAS_TOKEN_ADDRESS: Address = + crate::make_address(0x076dae45, 0xc8e16a92258252fe04dedd97f1ea93d6); +} + +pub mod events { + use crate::set_gas_token::consts; + use aurora_engine_types::types::Address; + use aurora_engine_types::H256; + use evm::backend::Log; + + // TODO + pub(crate) const SET_GAS_TOKEN_SIGNATURE: H256 = crate::make_h256( + 0x29d0b6eaa171d0d1607729f506329510, + 0x7bc9766ba17d250f129cb5bd06503d13, + ); + + pub(crate) struct SetGasTokenLog { + pub sender: Address, + pub gas_token: Address, + } + + impl SetGasTokenLog { + pub(crate) fn encode(self) -> Log { + let data = ethabi::encode(&[ethabi::Token::Address(self.gas_token.raw())]); + let sender_address = { + let mut buf = [0u8; 32]; + buf[12..].copy_from_slice(self.sender.as_bytes()); + H256(buf) + }; + let topics = vec![SET_GAS_TOKEN_SIGNATURE, sender_address]; + + let raw_log = ethabi::RawLog { topics, data }; + + Log { + address: consts::SET_GAS_TOKEN_ADDRESS.raw(), + topics: raw_log.topics, + data: raw_log.data, + } + } + } + + #[cfg(test)] + pub fn set_gas_token_schema() -> ethabi::Event { + ethabi::Event { + name: "SetGasToken".into(), + inputs: vec![ + ethabi::EventParam { + name: "sender".into(), + kind: ethabi::ParamType::Address, + indexed: true, + }, + ethabi::EventParam { + name: "gas_token".into(), + kind: ethabi::ParamType::Address, + indexed: true, + }, + ], + anonymous: false, + } + } +} + +/// A precompile contract used to set the gas token. +/// +/// Takes an input which must be an approved ERC-20 contract, or ETH itself at +/// the address `0x0`. +pub struct SetGasToken; + +impl SetGasToken { + pub const ADDRESS: Address = SET_GAS_TOKEN_ADDRESS; +} + +impl Precompile for SetGasToken { + fn required_gas(_input: &[u8]) -> Result { + Ok(costs::SET_GAS_TOKEN_GAS) + } + + fn run( + &self, + input: &[u8], + target_gas: Option, + context: &Context, + is_static: bool, + ) -> EvmPrecompileResult { + let required_gas = Self::required_gas(input)?; + if let Some(target_gas) = target_gas { + if required_gas > target_gas { + return Err(ExitError::OutOfGas); + } + } + + // It's not allowed to call exit precompiles in static mode + if is_static { + return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_STATIC"))); + } else if context.address != Self::ADDRESS.raw() { + return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_DELEGATE"))); + } + + let set_gas_token_log: Log = { + let sender = Address::new(context.caller); + let gas_token = Address::try_from_slice(input) + .map_err(|_e| ExitError::Other(Cow::from("ERR_INVALID_ETH_ADDRESS")))?; + SetGasTokenLog { sender, gas_token }.encode() + }; + + Ok(PrecompileOutput { + cost: required_gas, + logs: vec![set_gas_token_log], + ..Default::default() + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aurora_engine_sdk::types::near_account_to_evm_address; + + #[test] + fn test_precompile_id() { + assert_eq!( + SET_GAS_TOKEN_ADDRESS, + near_account_to_evm_address("setGasToken".as_bytes()) + ); + } + + #[test] + fn test_signature() { + let schema = events::set_gas_token_schema(); + assert_eq!(schema.signature(), events::SET_GAS_TOKEN_SIGNATURE); + } +} diff --git a/engine-types/src/types/address.rs b/engine-types/src/types/address.rs index 13be5223d..6d3b6135d 100755 --- a/engine-types/src/types/address.rs +++ b/engine-types/src/types/address.rs @@ -4,6 +4,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +const ADDRESS_HEX_LENGTH: usize = 40; +const ADDRESS_BYTE_LENGTH: usize = 20; + /// Base Eth Address type #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -26,10 +29,10 @@ impl Address { } pub fn decode(address: &str) -> Result { - if address.len() != 40 { + if address.len() != ADDRESS_HEX_LENGTH { return Err(error::AddressError::IncorrectLength); } - let mut result = [0u8; 20]; + let mut result = [0u8; ADDRESS_BYTE_LENGTH]; hex::decode_to_slice(address, &mut result) .map_err(|_| error::AddressError::FailedDecodeHex)?; Ok(Address::new(H160(result))) @@ -40,18 +43,25 @@ impl Address { } pub fn try_from_slice(raw_addr: &[u8]) -> Result { - if raw_addr.len() != 20 { - return Err(error::AddressError::IncorrectLength); + use core::cmp::Ordering; + match raw_addr.len().cmp(&ADDRESS_BYTE_LENGTH) { + Ordering::Greater => Err(error::AddressError::IncorrectLength), + Ordering::Less => { + let mut buf = [0u8; ADDRESS_BYTE_LENGTH]; + let pos = ADDRESS_BYTE_LENGTH - raw_addr.len(); + buf[pos..].copy_from_slice(raw_addr); + Ok(Self::new(H160::from_slice(&buf))) + } + Ordering::Equal => Ok(Self::new(H160::from_slice(raw_addr))), } - Ok(Self::new(H160::from_slice(raw_addr))) } - pub const fn from_array(array: [u8; 20]) -> Self { + pub const fn from_array(array: [u8; ADDRESS_BYTE_LENGTH]) -> Self { Self(H160(array)) } pub const fn zero() -> Self { - Address::new(H160([0u8; 20])) + Address::new(H160([0u8; ADDRESS_BYTE_LENGTH])) } } @@ -71,15 +81,15 @@ impl BorshSerialize for Address { impl BorshDeserialize for Address { fn deserialize(buf: &mut &[u8]) -> io::Result { - if buf.len() < 20 { + if buf.len() < ADDRESS_BYTE_LENGTH { return Err(io::Error::new( io::ErrorKind::Other, format!("{}", error::AddressError::IncorrectLength), )); } // Guaranty no panics. The length checked early - let address = Self(H160::from_slice(&buf[..20])); - *buf = &buf[20..]; + let address = Self(H160::from_slice(&buf[..ADDRESS_BYTE_LENGTH])); + *buf = &buf[ADDRESS_BYTE_LENGTH..]; Ok(address) } } @@ -139,8 +149,19 @@ mod tests { } #[test] - fn test_wrong_address_19() { - let serialized_addr = [0u8; 19]; + fn test_address_less_than_20_byte_length() { + let serialized_addr = [0x1u8; 1]; + let addr = Address::try_from_slice(&serialized_addr).unwrap(); + let expected = Address::try_from_slice( + &hex::decode("0000000000000000000000000000000000000001").unwrap(), + ) + .unwrap(); + assert_eq!(addr, expected); + } + + #[test] + fn test_address_greater_than_20_byte_length() { + let serialized_addr = [0x11u8; 21]; let addr = Address::try_from_slice(&serialized_addr); let err = addr.unwrap_err(); matches!(err, error::AddressError::IncorrectLength); diff --git a/engine-types/src/types/gas.rs b/engine-types/src/types/gas.rs index 0232b776d..b715faf70 100644 --- a/engine-types/src/types/gas.rs +++ b/engine-types/src/types/gas.rs @@ -56,6 +56,8 @@ impl Display for EthGas { } impl EthGas { + pub const MAX: EthGas = EthGas(u64::MAX); + /// Constructs a new `EthGas` with a given u64 value. pub const fn new(gas: u64) -> EthGas { Self(gas) diff --git a/engine/src/engine.rs b/engine/src/engine.rs index d1ac3aa47..7a363afbc 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -1,7 +1,7 @@ use crate::parameters::{CallArgs, NEP141FtOnTransferArgs, ResultLog, SubmitResult, ViewCallArgs}; use core::mem; use evm::backend::{Apply, ApplyBackend, Backend, Basic, Log}; -use evm::executor; +use evm::{executor, Context}; use evm::{Config, CreateScheme, ExitError, ExitFatal, ExitReason}; use crate::connector::EthConnectorContract; @@ -20,14 +20,16 @@ use crate::pausables::{ use crate::prelude::parameters::RefundCallArgs; use crate::prelude::precompiles::native::{exit_to_ethereum, exit_to_near}; use crate::prelude::precompiles::xcc::cross_contract_call; -use crate::prelude::precompiles::Precompiles; +use crate::prelude::precompiles::{self, Precompiles}; use crate::prelude::transactions::{EthTransactionKind, NormalizedEthTransaction}; use crate::prelude::{ address_to_key, bytes_to_key, sdk, storage_to_key, u256_to_arr, vec, AccountId, Address, BTreeMap, BorshDeserialize, BorshSerialize, KeyPrefix, PromiseArgs, PromiseCreateArgs, ToString, Vec, Wei, Yocto, ERC20_MINT_SELECTOR, H160, H256, U256, }; -use aurora_engine_precompiles::PrecompileConstructorContext; +use aurora_engine_precompiles::set_gas_token::SetGasToken; +use aurora_engine_precompiles::{Precompile, PrecompileConstructorContext}; +use aurora_engine_types::types::EthGas; use core::cell::RefCell; use core::iter::once; @@ -422,7 +424,6 @@ pub enum GasToken { ERC20(Address), } - pub struct Engine<'env, I: IO, E: Env> { state: EngineState, origin: Address, @@ -510,6 +511,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { } GasToken::ERC20(addr) => { // TODO: Needs SputnikVM balance check + todo!() } } @@ -529,7 +531,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { ) -> EngineResult { let origin = Address::new(self.origin()); let value = Wei::zero(); - self.deploy_code(origin, value, input, u64::MAX, Vec::new(), handler) + self.deploy_code(origin, value, input, EthGas::MAX, Vec::new(), handler) } pub fn deploy_code( @@ -537,20 +539,25 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { origin: Address, value: Wei, input: Vec, - gas_limit: u64, + gas_limit: EthGas, access_list: Vec<(H160, Vec)>, // See EIP-2930 handler: &mut P, ) -> EngineResult { let pause_flags = EnginePrecompilesPauser::from_io(self.io).paused(); let precompiles = self.create_precompiles(pause_flags, handler); - let executor_params = StackExecutorParams::new(gas_limit, precompiles); + let executor_params = StackExecutorParams::new(gas_limit.as_u64(), precompiles); let mut executor = executor_params.make_executor(self); let address = executor.create_address(CreateScheme::Legacy { caller: origin.raw(), }); - let (exit_reason, return_value) = - executor.transact_create(origin.raw(), value.raw(), input, gas_limit, access_list); + let (exit_reason, return_value) = executor.transact_create( + origin.raw(), + value.raw(), + input, + gas_limit.as_u64(), + access_list, + ); let result = if exit_reason.is_succeed() { address.0.to_vec() } else { @@ -591,7 +598,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { &contract, value, input, - u64::MAX, + EthGas::MAX, Vec::new(), handler, ) @@ -605,7 +612,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { &contract, value, input, - u64::MAX, + EthGas::MAX, Vec::new(), handler, ) @@ -620,21 +627,21 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { contract: &Address, value: Wei, input: Vec, - gas_limit: u64, + gas_limit: EthGas, access_list: Vec<(H160, Vec)>, // See EIP-2930 handler: &mut P, ) -> EngineResult { let pause_flags = EnginePrecompilesPauser::from_io(self.io).paused(); let precompiles = self.create_precompiles(pause_flags, handler); - let executor_params = StackExecutorParams::new(gas_limit, precompiles); + let executor_params = StackExecutorParams::new(gas_limit.as_u64(), precompiles); let mut executor = executor_params.make_executor(self); let (exit_reason, result) = executor.transact_call( origin.raw(), contract.raw(), value.raw(), input, - gas_limit, + gas_limit.as_u64(), access_list, ); @@ -740,7 +747,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { sender: Address, receiver: Address, value: Wei, - gas_limit: u64, + gas_limit: EthGas, handler: &mut P, ) -> EngineResult { self.call( @@ -873,8 +880,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { promise_handler: ro_promise_handler, }); // Cross contract calls are not enabled on mainnet yet. - tmp.all_precompiles - .remove(&aurora_engine_precompiles::xcc::cross_contract_call::ADDRESS); + tmp.all_precompiles.remove(&cross_contract_call::ADDRESS); tmp } else { Precompiles::new_london(PrecompileConstructorContext { @@ -953,7 +959,7 @@ pub fn submit( check_nonce(&io, &sender, &transaction.nonce)?; // Check intrinsic gas is covered by transaction gas limit - match transaction.intrinsic_gas(crate::engine::CONFIG) { + match transaction.intrinsic_gas(CONFIG) { Err(_e) => { return Err(EngineErrorKind::GasOverflow.into()); } @@ -969,7 +975,8 @@ pub fn submit( } let mut engine = Engine::new_with_state(state, sender, current_account_id, io, env); - let prepaid_amount = match engine.charge_gas(&sender, &transaction) { + // TODO: Have GasToken derived from storage. + let prepaid_amount = match engine.charge_gas(&sender, &transaction, GasToken::ETH) { Ok(gas_result) => gas_result, Err(GasPaymentError::OutOfFund) => { let result = SubmitResult::new(TransactionStatus::OutOfFund, 0, vec![]); @@ -979,7 +986,7 @@ pub fn submit( return Err(EngineErrorKind::GasPayment(err).into()); } }; - let gas_limit: u64 = transaction + let gas_limit: EthGas = transaction .gas_limit .try_into() .map_err(|_| EngineErrorKind::GasOverflow)?; @@ -989,16 +996,41 @@ pub fn submit( .map(|a| (a.address, a.storage_keys)) .collect(); let result = if let Some(receiver) = transaction.to { - engine.call( - &sender, - &receiver, - transaction.value, - transaction.data, - gas_limit, - access_list, - handler, - ) - // TODO: charge for storage + // We are making this precompile here special for now as we want to + // ensure that it can only be directly called. This may change in the + // future after careful research and consideration. For example, we are + // not sure if there are consequences in changing the gas during the + // execution of the transaction. + if receiver == precompiles::set_gas_token::SET_GAS_TOKEN_ADDRESS { + let set_gas_token = SetGasToken; + let context = Context { + address: receiver.raw(), + caller: transaction.address.raw(), + apparent_value: Default::default(), + }; + match set_gas_token.run(&transaction.data, Some(gas_limit), &context, false) { + Ok(v) => { + let tx_status = TransactionStatus::Succeed(vec![]); + let mut result_logs = Vec::new(); + for log in v.logs { + result_logs.push(log.into()); + } + Ok(SubmitResult::new(tx_status, v.cost.as_u64(), result_logs)) + } + Err(e) => Err(EngineError::from(EngineErrorKind::EvmError(e))), + } + } else { + engine.call( + &sender, + &receiver, + transaction.value, + transaction.data, + gas_limit, + access_list, + handler, + ) + // TODO: charge for storage + } } else { // Execute a contract deployment: engine.deploy_code( @@ -1062,14 +1094,14 @@ pub fn refund_on_error( &erc20_address, Wei::zero(), input, - u64::MAX, + EthGas::MAX, Vec::new(), handler, ) } // ETH exit; transfer ETH back from precompile address None => { - let exit_address = aurora_engine_precompiles::native::exit_to_near::ADDRESS; + let exit_address = exit_to_near::ADDRESS; let mut engine = Engine::new_with_state(state, exit_address, current_account_id, io, env); let refund_address = args.recipient_address; @@ -1079,7 +1111,7 @@ pub fn refund_on_error( &refund_address, amount, Vec::new(), - u64::MAX, + EthGas::MAX, vec![ (exit_address.raw(), Vec::new()), (refund_address.raw(), Vec::new()), @@ -1957,7 +1989,7 @@ mod tests { let mut engine = Engine::new_with_state(EngineState::default(), origin, current_account_id, io, &env); - let gas_limit = u64::MAX; + let gas_limit = EthGas::MAX; let mut handler = Noop; let receiver = make_address(1, 1); let value = Wei::new_u64(1000); diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 67e52b23d..ff133643a 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -30,7 +30,6 @@ pub mod log_entry; pub mod pausables; mod prelude; pub mod xcc; -mod erc20; #[cfg(target_arch = "wasm32")] #[global_allocator]