diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index fc6d9cc4e..622c123b1 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)) } } @@ -206,7 +207,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), @@ -235,7 +236,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), @@ -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), @@ -306,7 +307,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..13614d7cf --- /dev/null +++ b/engine-precompiles/src/set_gas_token.rs @@ -0,0 +1,175 @@ +use crate::prelude::{vec, Cow}; +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}; + +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::prelude::vec; + use crate::set_gas_token::consts; + use aurora_engine_types::types::Address; + use aurora_engine_types::H256; + use evm::backend::Log; + + pub 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 gas_token_address = { + let mut buf = [0u8; 32]; + buf[12..].copy_from_slice(self.gas_token.as_bytes()); + H256(buf) + }; + let data = ethabi::encode(&[ethabi::Token::Address(self.sender.raw())]); + let topics = vec![SET_GAS_TOKEN_SIGNATURE, gas_token_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 crate::prelude::H160; + 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); + } + + #[test] + fn test_run() { + let set_gas_token = SetGasToken; + let user = H160([0x11u8; 20]); + let target_gas = EthGas::new(10_000); + let context = Context { + address: SET_GAS_TOKEN_ADDRESS.raw(), + caller: user, + apparent_value: Default::default(), + }; + let input = hex::decode("8BEc47865aDe3B172A928df8f990Bc7f2A3b9f79").unwrap(); // Aurora mainnet contract address + let result = set_gas_token + .run(&input, Some(target_gas), &context, false) + .unwrap(); + println!("{result:?}"); + } +} diff --git a/engine-sdk/src/io.rs b/engine-sdk/src/io.rs index 7aeb7aaa8..6742f9b7e 100644 --- a/engine-sdk/src/io.rs +++ b/engine-sdk/src/io.rs @@ -156,7 +156,7 @@ pub trait IO { Ok(U256::from_big_endian(&result)) } - fn write_borsh( + fn write_storage_borsh( &mut self, key: &[u8], value: &T, diff --git a/engine-standalone-storage/src/relayer_db/mod.rs b/engine-standalone-storage/src/relayer_db/mod.rs index a528982ce..9cfb4466a 100644 --- a/engine-standalone-storage/src/relayer_db/mod.rs +++ b/engine-standalone-storage/src/relayer_db/mod.rs @@ -198,6 +198,7 @@ pub mod error { mod test { use super::FallibleIterator; use crate::sync::types::{TransactionKind, TransactionMessage}; + use aurora_engine::state::EngineStateV2; use aurora_engine::{connector, parameters, state}; use aurora_engine_types::H256; @@ -210,12 +211,12 @@ mod test { fn test_fill_db() { let mut storage = crate::Storage::open("rocks_tmp/").unwrap(); let mut connection = super::connect_without_tls(&Default::default()).unwrap(); - let engine_state = state::EngineState { + let engine_state = state::EngineState::V2(EngineStateV2 { chain_id: aurora_engine_types::types::u256_to_arr(&1313161555.into()), owner_id: "aurora".parse().unwrap(), - bridge_prover_id: "prover.bridge.near".parse().unwrap(), upgrade_delay_blocks: 0, - }; + default_gas_token: Default::default(), + }); // Initialize engine and connector states in storage. // Use explicit scope so borrows against `storage` are dropped before processing DB rows. @@ -234,9 +235,9 @@ mod test { state::set_state(&mut local_io, engine_state.clone()).unwrap(); connector::EthConnectorContract::create_contract( io, - engine_state.owner_id.clone(), + engine_state.owner_id(), parameters::InitCallArgs { - prover_account: engine_state.bridge_prover_id.clone(), + prover_account: "prover.bridge.near".parse().unwrap(), eth_custodian_address: "6bfad42cfc4efc96f529d786d643ff4a8b89fa52" .to_string(), metadata: Default::default(), diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 006d1aaba..1a83a5ab5 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -420,9 +420,9 @@ pub enum ConsumeMessageOutcome { #[derive(Debug)] pub struct TransactionIncludedOutcome { - pub hash: aurora_engine_types::H256, + pub hash: H256, pub info: TransactionMessage, - pub diff: crate::Diff, + pub diff: Diff, pub maybe_result: Result, error::Error>, } diff --git a/engine-tests/src/test_utils/mod.rs b/engine-tests/src/test_utils/mod.rs index ade752843..1ffe266fa 100644 --- a/engine-tests/src/test_utils/mod.rs +++ b/engine-tests/src/test_utils/mod.rs @@ -615,8 +615,8 @@ pub(crate) fn deploy_evm() -> AuroraRunner { let args = NewCallArgs { chain_id: crate::prelude::u256_to_arr(&U256::from(runner.chain_id)), owner_id: str_to_account_id(runner.aurora_account_id.as_str()), - bridge_prover_id: str_to_account_id("bridge_prover.near"), upgrade_delay_blocks: 1, + default_gas_token: [0u8; 20], }; let account_id = runner.aurora_account_id.clone(); @@ -821,8 +821,7 @@ pub(crate) fn as_account_id(account_id: &str) -> near_primitives_core::types::Ac } pub(crate) fn str_to_account_id(account_id: &str) -> AccountId { - use aurora_engine_types::str::FromStr; - AccountId::from_str(account_id).unwrap() + AccountId::new(account_id).unwrap() } pub fn unwrap_success(result: SubmitResult) -> Vec { diff --git a/engine-tests/src/test_utils/standalone/mocks/mod.rs b/engine-tests/src/test_utils/standalone/mocks/mod.rs index cfb5ec29d..0f761c6eb 100644 --- a/engine-tests/src/test_utils/standalone/mocks/mod.rs +++ b/engine-tests/src/test_utils/standalone/mocks/mod.rs @@ -17,7 +17,7 @@ pub const ETH_CUSTODIAN_ADDRESS: Address = aurora_engine_precompiles::make_address(0xd045f7e1, 0x9b2488924b97f9c145b5e51d0d895a65); pub fn compute_block_hash(block_height: u64) -> H256 { - aurora_engine::engine::compute_block_hash([0u8; 32], block_height, b"aurora") + engine::compute_block_hash([0u8; 32], block_height, b"aurora") } pub fn insert_block(storage: &mut Storage, block_height: u64) { @@ -52,8 +52,8 @@ pub fn init_evm(mut io: I, env: &E, chain_id: u64) { let new_args = NewCallArgs { chain_id: aurora_engine_types::types::u256_to_arr(&U256::from(chain_id)), owner_id: env.current_account_id(), - bridge_prover_id: test_utils::str_to_account_id("bridge_prover.near"), upgrade_delay_blocks: 1, + default_gas_token: [0u8; 20], // Base coin }; state::set_state(&mut io, new_args.into()).unwrap(); diff --git a/engine-tests/src/tests/eth_connector.rs b/engine-tests/src/tests/eth_connector.rs index c0d0274d6..1c2a6adf1 100644 --- a/engine-tests/src/tests/eth_connector.rs +++ b/engine-tests/src/tests/eth_connector.rs @@ -13,7 +13,6 @@ use aurora_engine_types::types::{Fee, NEP141Wei}; 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}; use serde_json::json; @@ -71,8 +70,8 @@ fn init_contract( &NewCallArgs { chain_id: [0u8; 32], owner_id: str_to_account_id(master_account.account_id.clone().as_str()), - bridge_prover_id: str_to_account_id(accounts(0).as_str()), upgrade_delay_blocks: 1, + default_gas_token: [0u8; 20], } .try_to_vec() .unwrap(), diff --git a/engine-tests/src/tests/sanity.rs b/engine-tests/src/tests/sanity.rs index 5e1e9ecea..cb7766765 100644 --- a/engine-tests/src/tests/sanity.rs +++ b/engine-tests/src/tests/sanity.rs @@ -136,15 +136,16 @@ fn test_state_format() { let args = aurora_engine::parameters::NewCallArgs { chain_id: aurora_engine_types::types::u256_to_arr(&666.into()), owner_id: "boss".parse().unwrap(), - bridge_prover_id: "prover_mcprovy_face".parse().unwrap(), upgrade_delay_blocks: 3, + default_gas_token: [255u8; 20], }; let state: aurora_engine::state::EngineState = args.into(); let expected_hex: String = [ + "00", // V2 - "00", V1 - "01", "000000000000000000000000000000000000000000000000000000000000029a", "04000000626f7373", - "1300000070726f7665725f6d6370726f76795f66616365", "0300000000000000", + "01ffffffffffffffffffffffffffffffffffffffff", ] .concat(); assert_eq!(hex::encode(state.try_to_vec().unwrap()), expected_hex); diff --git a/engine-tests/src/tests/standalone/sanity.rs b/engine-tests/src/tests/standalone/sanity.rs index bc9566a83..187d32372 100644 --- a/engine-tests/src/tests/standalone/sanity.rs +++ b/engine-tests/src/tests/standalone/sanity.rs @@ -1,8 +1,9 @@ +use aurora_engine::state::EngineStateV2; use aurora_engine::{engine, state}; use aurora_engine_sdk::env::DEFAULT_PREPAID_GAS; use aurora_engine_test_doubles::io::{Storage, StoragePointer}; use aurora_engine_test_doubles::promise::PromiseTracker; -use aurora_engine_types::types::{Address, Wei}; +use aurora_engine_types::types::{Address, EthGas, Wei}; use aurora_engine_types::{account_id::AccountId, H160, H256, U256}; use std::cell::RefCell; @@ -15,12 +16,12 @@ fn test_deploy_code() { buf }; let owner_id: AccountId = "aurora".parse().unwrap(); - let state = state::EngineState { + let state = state::EngineState::V2(EngineStateV2 { chain_id, owner_id: owner_id.clone(), - bridge_prover_id: "mr_the_prover".parse().unwrap(), upgrade_delay_blocks: 0, - }; + default_gas_token: Default::default(), + }); let origin = Address::new(H160([0u8; 20])); let storage = RefCell::new(Storage::default()); let io = StoragePointer(&storage); @@ -41,7 +42,7 @@ fn test_deploy_code() { origin, Wei::zero(), evm_deploy(&code_to_deploy), - u64::MAX, + EthGas::MAX, Vec::new(), &mut handler, ); diff --git a/engine-tests/src/tests/state_migration.rs b/engine-tests/src/tests/state_migration.rs index e175e0fca..4fe8bc90d 100644 --- a/engine-tests/src/tests/state_migration.rs +++ b/engine-tests/src/tests/state_migration.rs @@ -41,8 +41,8 @@ pub fn deploy_evm() -> AuroraAccount { let new_args = NewCallArgs { chain_id: crate::prelude::u256_to_arr(&U256::from(aurora_runner.chain_id)), owner_id: str_to_account_id(main_account.account_id.as_str()), - bridge_prover_id: prover_account.clone(), upgrade_delay_blocks: 1, + default_gas_token: [0u8; 20], }; main_account .call( diff --git a/engine-types/src/storage.rs b/engine-types/src/storage.rs index e1b1be281..c37ca498b 100644 --- a/engine-types/src/storage.rs +++ b/engine-types/src/storage.rs @@ -30,6 +30,7 @@ pub enum KeyPrefix { Nep141Erc20Map = 0x8, Erc20Nep141Map = 0x9, CrossContractCall = 0xa, + GasToken = 0xb, } impl From for u8 { @@ -47,6 +48,7 @@ impl From for u8 { Nep141Erc20Map => 0x8, Erc20Nep141Map => 0x9, CrossContractCall => 0xa, + GasToken => 0xb, } } } diff --git a/engine-types/src/types/address.rs b/engine-types/src/types/address.rs index 2eaeeb7a7..da9541dd8 100755 --- a/engine-types/src/types/address.rs +++ b/engine-types/src/types/address.rs @@ -3,6 +3,9 @@ use borsh::maybestd::io; use borsh::{BorshDeserialize, BorshSerialize}; 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, Serialize, Deserialize)] pub struct Address(H160); @@ -24,10 +27,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))) @@ -38,18 +41,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])) } } @@ -69,15 +79,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) } } @@ -137,8 +147,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 cd9bc0db1..28b47f5e6 100644 --- a/engine-types/src/types/gas.rs +++ b/engine-types/src/types/gas.rs @@ -54,6 +54,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-types/src/types/mod.rs b/engine-types/src/types/mod.rs index 82e666199..2efe0b2b4 100644 --- a/engine-types/src/types/mod.rs +++ b/engine-types/src/types/mod.rs @@ -133,10 +133,6 @@ impl Stack { } } -pub fn str_from_slice(inp: &[u8]) -> &str { - str::from_utf8(inp).unwrap() -} - #[cfg(test)] mod tests { use super::*; diff --git a/engine/src/connector.rs b/engine/src/connector.rs index 21b359643..08edb30d4 100644 --- a/engine/src/connector.rs +++ b/engine/src/connector.rs @@ -104,7 +104,7 @@ impl EthConnectorContract { ft.internal_register_account(&owner_id); let paused_mask = UNPAUSE_ALL; - io.write_borsh( + io.write_storage_borsh( &construct_contract_key(&EthConnectorStorageId::PausedMask), &paused_mask, ); @@ -618,7 +618,7 @@ impl EthConnectorContract { /// Save eth-connector fungible token contract data fn save_ft_contract(&mut self) { - self.io.write_borsh( + self.io.write_storage_borsh( &construct_contract_key(&EthConnectorStorageId::FungibleToken), &self.ft.data(), ); @@ -633,7 +633,7 @@ impl EthConnectorContract { /// Save already used event proof as hash key fn save_used_event(&mut self, key: &str) { - self.io.write_borsh(&self.used_event_key(key), &0u8); + self.io.write_storage_borsh(&self.used_event_key(key), &0u8); } /// Check is event of proof already used @@ -666,7 +666,7 @@ impl AdminControlled for EthConnectorContract { /// Set admin paused status fn set_paused(&mut self, paused_mask: PausedMask) { self.paused_mask = paused_mask; - self.io.write_borsh( + self.io.write_storage_borsh( &construct_contract_key(&EthConnectorStorageId::PausedMask), &self.paused_mask, ); @@ -700,12 +700,12 @@ pub fn set_contract_data( eth_custodian_address: Address::decode(&args.eth_custodian_address)?, }; // Save eth-connector specific data - io.write_borsh( + io.write_storage_borsh( &construct_contract_key(&EthConnectorStorageId::Contract), &contract_data, ); - io.write_borsh( + io.write_storage_borsh( &construct_contract_key(&EthConnectorStorageId::FungibleTokenMetadata), &args.metadata, ); diff --git a/engine/src/deposit_event.rs b/engine/src/deposit_event.rs index 496d24a2e..dc34a4fb0 100644 --- a/engine/src/deposit_event.rs +++ b/engine/src/deposit_event.rs @@ -527,10 +527,8 @@ mod tests { let parse_error = TokenMessageData::parse_event_message_and_prepare_token_message_data(message, fee) .unwrap_err(); - let actual_parse_error = std::str::from_utf8(parse_error.as_ref()).unwrap(); - let expected_parse_error = AddressError::IncorrectLength.to_string(); - assert_eq!(expected_parse_error, actual_parse_error); + assert_eq!(parse_error.as_ref(), AddressError::IncorrectLength.as_ref()); } #[test] @@ -541,10 +539,8 @@ mod tests { let parse_error = TokenMessageData::parse_event_message_and_prepare_token_message_data(message, fee) .unwrap_err(); - let actual_parse_error = std::str::from_utf8(parse_error.as_ref()).unwrap(); - let expected_parse_error = AddressError::FailedDecodeHex.to_string(); - assert_eq!(expected_parse_error, actual_parse_error); + assert_eq!(parse_error.as_ref(), AddressError::FailedDecodeHex.as_ref()); } #[test] diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 35d2c64a2..c46cfa208 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -1,26 +1,19 @@ -use crate::parameters::{CallArgs, NEP141FtOnTransferArgs, ResultLog, SubmitResult, ViewCallArgs}; -use core::mem; -use evm::backend::{Apply, ApplyBackend, Backend, Basic, Log}; -use evm::executor; -use evm::{Config, CreateScheme, ExitError, ExitFatal, ExitReason}; - use crate::connector::EthConnectorContract; use crate::map::BijectionMap; -use crate::{errors, state}; +use crate::{errors, gas_token, state}; use aurora_engine_sdk::caching::FullCache; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::{StorageIntermediate, IO}; use aurora_engine_sdk::promise::{PromiseHandler, PromiseId, ReadOnlyPromiseHandler}; use crate::accounting; -use crate::parameters::{DeployErc20TokenArgs, TransactionStatus}; +use crate::gas_token::GasToken; +use crate::parameters::{DeployErc20TokenArgs, NEP141FtOnTransferArgs, TransactionStatus}; use crate::pausables::{ EngineAuthorizer, EnginePrecompilesPauser, PausedPrecompilesChecker, PrecompileFlags, }; 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, @@ -28,9 +21,15 @@ use crate::prelude::{ Yocto, ERC20_MINT_SELECTOR, H160, H256, U256, }; use crate::state::EngineState; -use aurora_engine_precompiles::PrecompileConstructorContext; +use aurora_engine_precompiles::{Precompile, PrecompileConstructorContext}; +use aurora_engine_types::parameters::engine::{CallArgs, ResultLog, SubmitResult, ViewCallArgs}; +use aurora_engine_types::types::EthGas; use core::cell::RefCell; use core::iter::once; +use core::mem; +use evm::backend::{Apply, ApplyBackend, Backend, Basic, Log}; +use evm::{executor, Context}; +use evm::{Config, CreateScheme, ExitError, ExitFatal, ExitReason}; /// Used as the first byte in the concatenation of data used to compute the blockhash. /// Could be useful in the future as a version byte, or to distinguish different types of blocks. @@ -413,10 +412,13 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { } } + // TODO: If we don't actually charge_gas here after changes, this should + // be renamed. pub fn charge_gas( &mut self, sender: &Address, transaction: &NormalizedEthTransaction, + gas_token: GasToken, ) -> Result { if transaction.max_fee_per_gas.is_zero() { return Ok(GasPaymentResult::default()); @@ -432,11 +434,23 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { .map(Wei::new) .ok_or(GasPaymentError::EthAmountOverflow)?; - let new_balance = get_balance(&self.io, sender) - .checked_sub(prepaid_amount) - .ok_or(GasPaymentError::OutOfFund)?; - - set_balance(&mut self.io, sender, &new_balance); + match gas_token { + GasToken::Base => { + // TODO: Remove the set_balance as we need to just check if they can spend + // Use new `can_transfer` function instead. + // Also needs to be verified how it is done on geth. + let new_balance = get_balance(&self.io, sender) + .checked_sub(prepaid_amount) + .ok_or(GasPaymentError::OutOfFund)?; + + // This part is questionable. + set_balance(&mut self.io, sender, &new_balance); + } + GasToken::Erc20(_addr) => { + // TODO: Needs SputnikVM balance check + todo!() + } + } self.gas_price = effective_gas_price; @@ -454,7 +468,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( @@ -462,20 +476,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 { @@ -486,7 +505,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { let status = exit_reason.into_result(result)?; let (values, logs) = executor.into_state().deconstruct(); - let logs = filter_promises_from_logs(&self.io, handler, logs, &self.current_account_id); + let logs = apply_actions_from_logs(&mut self.io, handler, logs, &self.current_account_id); self.apply(values, Vec::::new(), true); @@ -510,7 +529,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { &contract, value, input, - u64::MAX, + EthGas::MAX, Vec::new(), handler, ) @@ -524,7 +543,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { &contract, value, input, - u64::MAX, + EthGas::MAX, Vec::new(), handler, ) @@ -539,21 +558,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, ); @@ -561,7 +580,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { let status = exit_reason.into_result(result)?; let (values, logs) = executor.into_state().deconstruct(); - let logs = filter_promises_from_logs(&self.io, handler, logs, &self.current_account_id); + let logs = apply_actions_from_logs(&mut self.io, handler, logs, &self.current_account_id); // There is no way to return the logs to the NEAR log method as it only // allows a return of UTF-8 strings. @@ -653,7 +672,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( @@ -720,7 +739,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { &erc20_token, Wei::zero(), setup_receive_erc20_tokens_input(args, &recipient), - u64::MAX, + EthGas::MAX, Vec::new(), // TODO: are there values we should put here? handler, ) @@ -787,7 +806,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { }); // Cross contract calls are not enabled on mainnet yet. tmp.all_precompiles - .remove(&aurora_engine_precompiles::xcc::cross_contract_call::ADDRESS); + .remove(&precompiles::xcc::cross_contract_call::ADDRESS); tmp } else { Precompiles::new_london(PrecompileConstructorContext { @@ -853,7 +872,7 @@ pub fn submit( // Validate the chain ID, if provided inside the signature: if let Some(chain_id) = transaction.chain_id { - if U256::from(chain_id) != U256::from(state.chain_id) { + if U256::from(chain_id) != U256::from(state.chain_id()) { return Err(EngineErrorKind::InvalidChainId.into()); } } @@ -866,7 +885,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()); } @@ -882,32 +901,61 @@ 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::Base) { Ok(gas_result) => gas_result, Err(err) => { return Err(EngineErrorKind::GasPayment(err).into()); } }; - let gas_limit: u64 = transaction - .gas_limit - .try_into() - .map_err(|_| EngineErrorKind::GasOverflow)?; + let gas_limit: EthGas = { + let gas_limit: u64 = transaction + .gas_limit + .try_into() + .map_err(|_| EngineErrorKind::GasOverflow)?; + EthGas::new(gas_limit) + }; let access_list = transaction .access_list .into_iter() .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 = precompiles::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(evm_log_to_result_log(log)); + } + 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( @@ -971,14 +1019,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 = precompiles::native::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; @@ -988,7 +1036,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()), @@ -1032,6 +1080,7 @@ pub fn get_authorizer() -> EngineAuthorizer { EngineAuthorizer::from_accounts(once(account)) } +// TODO: Gas must be charged from here. pub fn refund_unused_gas( io: &mut I, sender: &Address, @@ -1058,8 +1107,12 @@ pub fn refund_unused_gas( .checked_sub(spent_amount) .ok_or(GasPaymentError::EthAmountOverflow)?; - add_balance(io, sender, refund)?; - add_balance(io, relayer, reward_amount)?; + if refund > Wei::zero() { + add_balance(io, sender, refund)?; + } + if reward_amount > Wei::zero() { + add_balance(io, relayer, reward_amount)?; + } Ok(()) } @@ -1243,6 +1296,14 @@ pub fn get_balance(io: &I, address: &Address) -> Wei { Wei::new(raw) } +/// Checks whether there are enough funds for the given address and amount. +/// +/// This does not take gas into account. +pub fn can_transfer(io: &I, address: &Address, amount: Wei) -> bool { + let current_balance = get_balance(io, address); + amount >= current_balance +} + pub fn remove_storage(io: &mut I, address: &Address, key: &H256, generation: u32) { io.remove_storage(storage_to_key(address, key, generation).as_ref()); } @@ -1316,8 +1377,8 @@ fn remove_account(io: &mut I, address: &Address, generation: u32) remove_all_storage(io, address, generation); } -fn filter_promises_from_logs( - io: &I, +fn apply_actions_from_logs( + io: &mut I, handler: &mut P, logs: T, current_account_id: &AccountId, @@ -1329,8 +1390,8 @@ where { logs.into_iter() .filter_map(|log| { - if log.address == exit_to_near::ADDRESS.raw() - || log.address == exit_to_ethereum::ADDRESS.raw() + if log.address == precompiles::native::exit_to_near::ADDRESS.raw() + || log.address == precompiles::native::exit_to_ethereum::ADDRESS.raw() { if log.topics.is_empty() { if let Ok(promise) = PromiseArgs::try_from_slice(&log.data) { @@ -1362,8 +1423,8 @@ where // `topics` field. Some(evm_log_to_result_log(log)) } - } else if log.address == cross_contract_call::ADDRESS.raw() { - if log.topics[0] == cross_contract_call::AMOUNT_TOPIC { + } else if log.address == precompiles::xcc::cross_contract_call::ADDRESS.raw() { + if log.topics[0] == precompiles::xcc::cross_contract_call::AMOUNT_TOPIC { // NEAR balances are 128-bit, so the leading 16 bytes of the 256-bit topic // value should always be zero. assert_eq!(&log.topics[1].as_bytes()[0..16], &[0; 16]); @@ -1381,6 +1442,21 @@ where } // do not pass on these "internal logs" to caller None + } else if log.address == precompiles::set_gas_token::SET_GAS_TOKEN_ADDRESS.raw() { + if log.topics[0] == precompiles::set_gas_token::events::SET_GAS_TOKEN_SIGNATURE { + let sender_address = { + let mut buf = [0u8; 20]; + buf.copy_from_slice(&log.data[12..]); + Address::from_array(buf) + }; + let gas_token: GasToken = { + let mut buf = [0u8; 20]; + buf.copy_from_slice(&log.topics[1].as_bytes()[12..]); // No need to `get` here given we know the length + GasToken::from_address(Address::from_array(buf)) + }; + gas_token::set_gas_token(io, sender_address, gas_token); + } + None } else { Some(evm_log_to_result_log(log)) } @@ -1426,7 +1502,7 @@ unsafe fn schedule_promise_callback( handler.promise_attach_callback(base_id, promise) } -impl<'env, I: IO + Copy, E: Env> evm::backend::Backend for Engine<'env, I, E> { +impl<'env, I: IO + Copy, E: Env> Backend for Engine<'env, I, E> { /// Returns the "effective" gas price (as defined by EIP-1559) fn gas_price(&self) -> U256 { self.gas_price @@ -1459,7 +1535,7 @@ impl<'env, I: IO + Copy, E: Env> evm::backend::Backend for Engine<'env, I, E> { if idx.saturating_sub(U256::from(256)) <= number && number < idx { // since `idx` comes from `u64` it is always safe to downcast `number` from `U256` compute_block_hash( - self.state.chain_id, + self.state.chain_id(), number.low_u64(), self.current_account_id.as_bytes(), ) @@ -1519,7 +1595,7 @@ impl<'env, I: IO + Copy, E: Env> evm::backend::Backend for Engine<'env, I, E> { /// Returns the states chain ID. fn chain_id(&self) -> U256 { - U256::from(self.state.chain_id) + U256::from(self.state.chain_id()) } /// Checks if an address exists. @@ -1713,10 +1789,12 @@ impl<'env, J: IO + Copy, E: Env> ApplyBackend for Engine<'env, J, E> { } #[cfg(test)] +#[cfg(feature = "std")] mod tests { use super::*; use crate::parameters::{FunctionCallArgsV1, FunctionCallArgsV2}; use aurora_engine_precompiles::make_address; + use aurora_engine_precompiles::native::exit_to_near; use aurora_engine_sdk::env::Fixed; use aurora_engine_sdk::promise::Noop; use aurora_engine_test_doubles::io::{Storage, StoragePointer}; @@ -1846,7 +1924,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); @@ -1991,7 +2069,10 @@ mod tests { data: vec![], access_list: vec![], }; - let actual_result = engine.charge_gas(&origin, &transaction).unwrap(); + // TODO: Add other tests than just ETH as a gas token. + let actual_result = engine + .charge_gas(&origin, &transaction, GasToken::Base) + .unwrap(); let expected_result = GasPaymentResult { prepaid_amount: Wei::zero(), @@ -2216,7 +2297,7 @@ mod tests { #[test] fn test_filtering_promises_from_logs_with_none_keeps_all() { let storage = RefCell::new(Storage::default()); - let io = StoragePointer(&storage); + let mut io = StoragePointer(&storage); let current_account_id = AccountId::default(); let mut handler = Noop; let logs = vec![Log { @@ -2225,7 +2306,7 @@ mod tests { data: vec![], }]; - let actual_logs = filter_promises_from_logs(&io, &mut handler, logs, ¤t_account_id); + let actual_logs = apply_actions_from_logs(&mut io, &mut handler, logs, ¤t_account_id); let expected_logs = vec![ResultLog { address: Default::default(), topics: vec![], diff --git a/engine/src/errors.rs b/engine/src/errors.rs index c1afdb66a..6e993472d 100644 --- a/engine/src/errors.rs +++ b/engine/src/errors.rs @@ -50,8 +50,6 @@ pub const ERR_GAS_OVERFLOW: &[u8; 16] = b"ERR_GAS_OVERFLOW"; pub const ERR_BALANCE_OVERFLOW: &[u8; 20] = b"ERR_BALANCE_OVERFLOW"; pub const ERR_GAS_ETH_AMOUNT_OVERFLOW: &[u8; 27] = b"ERR_GAS_ETH_AMOUNT_OVERFLOW"; pub const ERR_PARSE_ADDRESS: &[u8; 17] = b"ERR_PARSE_ADDRESS"; -pub const ERR_STATE_NOT_FOUND: &[u8; 19] = b"ERR_STATE_NOT_FOUND"; -pub const ERR_STATE_CORRUPTED: &[u8; 19] = b"ERR_STATE_CORRUPTED"; pub const ERR_CONNECTOR_STORAGE_KEY_NOT_FOUND: &[u8; 35] = b"ERR_CONNECTOR_STORAGE_KEY_NOT_FOUND"; pub const ERR_FAILED_DESERIALIZE_CONNECTOR_DATA: &[u8; 37] = diff --git a/engine/src/fungible_token.rs b/engine/src/fungible_token.rs index 24f98ecc0..52636b4a3 100644 --- a/engine/src/fungible_token.rs +++ b/engine/src/fungible_token.rs @@ -496,7 +496,7 @@ impl FungibleTokenOps { self.io.write_storage(&key, &accounts_counter.to_le_bytes()); } self.io - .write_borsh(&Self::account_to_key(account_id), &amount); + .write_storage_borsh(&Self::account_to_key(account_id), &amount); } /// Get accounts counter for statistics diff --git a/engine/src/gas_token.rs b/engine/src/gas_token.rs new file mode 100644 index 000000000..6b8f70192 --- /dev/null +++ b/engine/src/gas_token.rs @@ -0,0 +1,57 @@ +use crate::prelude::{BorshDeserialize, BorshSerialize}; +use aurora_engine_sdk::io::{StorageIntermediate, IO}; +use aurora_engine_types::storage; +use aurora_engine_types::storage::KeyPrefix; +use aurora_engine_types::types::Address; + +mod errors { + pub const ERR_DESERIALIZE_GAS_TOKEN: &str = "ERR_DESERIALIZE_GAS_TOKEN"; +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +/// Used to select which gas token to pay in. +pub enum GasToken { + /// Gas is paid in Ether. + #[default] + Base, + /// Gas is paid in a ERC-20 compatible token. + Erc20(Address), +} + +impl GasToken { + // TODO: wait for use of this + // fn into_address(self) -> Address { + // use GasToken::*; + // match self { + // Base => Address::from_array([0u8; 20]), + // Erc20(addr) => addr, + // } + // } + + pub(crate) fn from_address(address: Address) -> GasToken { + use GasToken::*; + if address == Address::zero() { + Base + } else { + Erc20(address) + } + } + + pub(crate) fn from_array(bytes: [u8; 20]) -> GasToken { + Self::from_address(Address::from_array(bytes)) + } +} + +/// Sets the gas token for a given address and returns the old value, if any. +pub fn set_gas_token(io: &mut I, address: Address, gas_token: GasToken) -> Option { + let key = storage::bytes_to_key(KeyPrefix::GasToken, address.as_bytes()); + io.write_storage_borsh(&key, &gas_token) + .map(|v| v.to_value().expect(errors::ERR_DESERIALIZE_GAS_TOKEN)) +} + +/// Gets the gas token set for a given address, if any. +pub fn get_gas_token(io: &I, address: &Address) -> Option { + let key = storage::bytes_to_key(KeyPrefix::GasToken, address.as_bytes()); + io.read_storage(&key) + .map(|v| v.to_value().expect(errors::ERR_DESERIALIZE_GAS_TOKEN)) +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 78ea86349..08fae60b6 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -25,6 +25,7 @@ pub mod deposit_event; pub mod engine; pub mod errors; pub mod fungible_token; +pub mod gas_token; pub mod log_entry; pub mod pausables; mod prelude; @@ -93,7 +94,9 @@ mod contract { near_account_to_evm_address, SdkExpect, SdkProcess, SdkUnwrap, }; use crate::prelude::storage::{bytes_to_key, KeyPrefix}; - use crate::prelude::{sdk, u256_to_arr, Address, PromiseResult, Yocto, ERR_FAILED_PARSE, H256}; + use crate::prelude::{ + sdk, u256_to_arr, Address, PromiseResult, ToString, Yocto, ERR_FAILED_PARSE, H256, + }; use crate::{errors, pausables, state}; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::{StorageIntermediate, IO}; @@ -139,7 +142,7 @@ mod contract { pub extern "C" fn get_owner() { let mut io = Runtime; let state = state::get_state(&io).sdk_unwrap(); - io.return_output(state.owner_id.as_bytes()); + io.return_output(state.owner_id().as_bytes()); } /// Get bridge prover id for this contract. @@ -154,7 +157,7 @@ mod contract { #[no_mangle] pub extern "C" fn get_chain_id() { let mut io = Runtime; - io.return_output(&state::get_state(&io).sdk_unwrap().chain_id) + io.return_output(&state::get_state(&io).sdk_unwrap().chain_id()) } #[no_mangle] @@ -162,7 +165,7 @@ mod contract { let mut io = Runtime; let state = state::get_state(&io).sdk_unwrap(); let index = internal_get_upgrade_index(); - io.return_output(&(index + state.upgrade_delay_blocks).to_le_bytes()) + io.return_output(&(index + state.upgrade_delay_blocks()).to_le_bytes()) } /// Stage new code for deployment. @@ -186,7 +189,7 @@ mod contract { let state = state::get_state(&io).sdk_unwrap(); require_owner_only(&state, &io.predecessor_account_id()); let index = internal_get_upgrade_index(); - if io.block_height() <= index + state.upgrade_delay_blocks { + if io.block_height() <= index + state.upgrade_delay_blocks() { sdk::panic_utf8(errors::ERR_NOT_ALLOWED_TOO_EARLY); } Runtime::self_deploy(&bytes_to_key(KeyPrefix::Config, CODE_KEY)); @@ -196,9 +199,7 @@ mod contract { /// to make any necessary changes to the state such that it aligns with the newly deployed /// code. #[no_mangle] - pub extern "C" fn state_migration() { - // TODO: currently we don't have migrations - } + pub extern "C" fn state_migration() {} /// Resumes previously [`paused`] precompiles. /// @@ -474,7 +475,7 @@ mod contract { let block_height = io.read_input_borsh().sdk_unwrap(); let account_id = io.current_account_id(); let chain_id = state::get_state(&io) - .map(|state| state.chain_id) + .map(|state| state.chain_id()) .sdk_unwrap(); let block_hash = crate::engine::compute_block_hash(chain_id, block_height, account_id.as_bytes()); @@ -526,7 +527,7 @@ mod contract { let mut state = state::get_state(&io).sdk_unwrap(); require_owner_only(&state, &io.predecessor_account_id()); let args: BeginChainArgs = io.read_input_borsh().sdk_unwrap(); - state.chain_id = args.chain_id; + state.set_chain_id(args.chain_id); state::set_state(&mut io, state).sdk_unwrap(); // set genesis block balances for account_balance in args.genesis_alloc { @@ -537,7 +538,7 @@ mod contract { ) } // return new chain ID - io.return_output(&state::get_state(&io).sdk_unwrap().chain_id) + io.return_output(&state::get_state(&io).sdk_unwrap().chain_id()) } #[cfg(feature = "evm_bully")] @@ -906,7 +907,7 @@ mod contract { #[no_mangle] pub extern "C" fn mint_account() { use crate::connector::ZERO_ATTACHED_BALANCE; - use crate::prelude::{NEP141Wei, ToString, U256}; + use crate::prelude::{NEP141Wei, U256}; use evm::backend::ApplyBackend; const GAS_FOR_VERIFY: NearGas = NearGas::new(20_000_000_000_000); const GAS_FOR_FINISH: NearGas = NearGas::new(50_000_000_000_000); @@ -982,7 +983,7 @@ mod contract { } fn require_owner_only(state: &state::EngineState, predecessor_account_id: &AccountId) { - if &state.owner_id != predecessor_account_id { + if &state.owner_id() != predecessor_account_id { sdk::panic_utf8(errors::ERR_NOT_ALLOWED); } } diff --git a/engine/src/parameters.rs b/engine/src/parameters.rs index d03e0089b..b5f25f377 100644 --- a/engine/src/parameters.rs +++ b/engine/src/parameters.rs @@ -19,11 +19,10 @@ pub struct NewCallArgs { /// Account which can upgrade this contract. /// Use empty to disable updatability. pub owner_id: AccountId, - /// Account of the bridge prover. - /// Use empty to not use base token as bridged asset. - pub bridge_prover_id: AccountId, /// How many blocks after staging upgrade can deploy it. pub upgrade_delay_blocks: u64, + /// The default gas token. + pub default_gas_token: [u8; 20], } /// Borsh-encoded (genesis) account balance used by the `begin_chain` function. diff --git a/engine/src/state.rs b/engine/src/state.rs index 6023f4338..50948d3a1 100644 --- a/engine/src/state.rs +++ b/engine/src/state.rs @@ -1,3 +1,4 @@ +use crate::gas_token::GasToken; use crate::parameters::NewCallArgs; use aurora_engine_sdk::io::{StorageIntermediate, IO}; use aurora_engine_types::account_id::AccountId; @@ -10,9 +11,73 @@ pub use error::EngineStateError; const STATE_KEY: &[u8; 5] = b"STATE"; /// Engine internal state, mostly configuration. +#[derive(BorshSerialize, BorshDeserialize, Clone, PartialEq, Eq, Debug)] +pub enum EngineState { + V2(EngineStateV2), + V1(EngineStateV1), +} + +impl EngineState { + pub fn chain_id(&self) -> [u8; 32] { + match self { + EngineState::V2(state) => state.chain_id, + EngineState::V1(state) => state.chain_id, + } + } + + pub fn set_chain_id(&mut self, chain_id: [u8; 32]) { + match self { + EngineState::V2(state) => state.chain_id = chain_id, + EngineState::V1(state) => state.chain_id = chain_id, + } + } + + pub fn owner_id(&self) -> AccountId { + match self { + EngineState::V2(state) => state.owner_id.clone(), + EngineState::V1(state) => state.owner_id.clone(), + } + } + + pub fn upgrade_delay_blocks(&self) -> u64 { + match self { + EngineState::V2(state) => state.upgrade_delay_blocks, + EngineState::V1(state) => state.upgrade_delay_blocks, + } + } + + pub fn deserialize(bytes: &[u8]) -> Option { + Self::try_from_slice(bytes) + .or_else(|_| EngineStateV1::try_from_slice(bytes).map(Self::V1)) + .ok() + } +} + +impl Default for EngineState { + fn default() -> Self { + Self::V2(EngineStateV2::default()) + } +} + +/// Engine internal state V2, mostly configuration. /// Should not contain anything large or enumerable. #[derive(BorshSerialize, BorshDeserialize, Default, Clone, PartialEq, Eq, Debug)] -pub struct EngineState { +pub struct EngineStateV2 { + /// Chain id, according to the EIP-155 / ethereum-lists spec. + pub chain_id: [u8; 32], + /// Account which can upgrade this contract. + /// Use empty to disable updatability. + pub owner_id: AccountId, + /// How many blocks after staging upgrade can deploy it. + pub upgrade_delay_blocks: u64, + /// The default gas token. + pub default_gas_token: GasToken, +} + +/// Engine internal state V1, mostly configuration. +/// Should not contain anything large or enumerable. +#[derive(BorshSerialize, BorshDeserialize, Default, Clone, PartialEq, Eq, Debug)] +pub struct EngineStateV1 { /// Chain id, according to the EIP-155 / ethereum-lists spec. pub chain_id: [u8; 32], /// Account which can upgrade this contract. @@ -27,31 +92,32 @@ pub struct EngineState { impl From for EngineState { fn from(args: NewCallArgs) -> Self { - EngineState { + EngineState::V2(EngineStateV2 { chain_id: args.chain_id, owner_id: args.owner_id, - bridge_prover_id: args.bridge_prover_id, upgrade_delay_blocks: args.upgrade_delay_blocks, - } + default_gas_token: GasToken::from_array(args.default_gas_token), + }) } } /// Gets the state from storage, if it exists otherwise it will error. -pub fn get_state(io: &I) -> Result { +pub fn get_state(io: &I) -> Result { match io.read_storage(&bytes_to_key(KeyPrefix::Config, STATE_KEY)) { - None => Err(error::EngineStateError::NotFound), - Some(bytes) => EngineState::try_from_slice(&bytes.to_vec()) - .map_err(|_| error::EngineStateError::DeserializationFailed), + None => Err(EngineStateError::NotFound), + Some(bytes) => { + EngineState::deserialize(&bytes.to_vec()).ok_or(EngineStateError::DeserializationFailed) + } } } /// Saves state into the storage. Does not return the previous state. -pub fn set_state(io: &mut I, state: EngineState) -> Result<(), error::EngineStateError> { +pub fn set_state(io: &mut I, state: EngineState) -> Result<(), EngineStateError> { io.write_storage( &bytes_to_key(KeyPrefix::Config, STATE_KEY), &state .try_to_vec() - .map_err(|_| error::EngineStateError::SerializationFailed)?, + .map_err(|_| EngineStateError::SerializationFailed)?, ); Ok(()) @@ -96,12 +162,9 @@ mod tests { fn test_missing_engine_state_is_not_found() { let storage = RefCell::new(Storage::default()); let io = StoragePointer(&storage); - let actual_error = get_state(&io).unwrap_err(); - let actual_error = std::str::from_utf8(actual_error.as_ref()).unwrap(); - let expected_error = std::str::from_utf8(error::ERR_STATE_NOT_FOUND).unwrap(); - assert_eq!(expected_error, actual_error); + assert_eq!(actual_error.as_ref(), error::ERR_STATE_NOT_FOUND); } #[test] @@ -111,9 +174,7 @@ mod tests { io.write_storage(&bytes_to_key(KeyPrefix::Config, STATE_KEY), &[]); let actual_error = get_state(&io).unwrap_err(); - let actual_error = std::str::from_utf8(actual_error.as_ref()).unwrap(); - let expected_error = std::str::from_utf8(error::ERR_STATE_CORRUPTED).unwrap(); - assert_eq!(expected_error, actual_error); + assert_eq!(actual_error.as_ref(), error::ERR_STATE_CORRUPTED); } } diff --git a/etc/tests/state-migration-test/src/lib.rs b/etc/tests/state-migration-test/src/lib.rs index 08b82b09e..e6908b183 100644 --- a/etc/tests/state-migration-test/src/lib.rs +++ b/etc/tests/state-migration-test/src/lib.rs @@ -3,7 +3,7 @@ extern crate alloc; use alloc::vec::Vec; -use aurora_engine::state; +use aurora_engine::state::{self, EngineState}; use aurora_engine_sdk::near_runtime::Runtime; use aurora_engine_sdk::io::{IO, StorageIntermediate}; use aurora_engine_types::storage;