From 66eba13e36e8ae48dd8f956ad58bfbf3f93c2ba1 Mon Sep 17 00:00:00 2001 From: Marcelo Fornet Date: Mon, 25 Jul 2022 14:54:33 +0200 Subject: [PATCH 01/30] Add cross contract call support in the engine --- XCC.md | 27 ++++++++++++ engine-precompiles/src/lib.rs | 7 +++ engine-precompiles/src/xcc.rs | 83 +++++++++++++++++++++++++++++++++++ engine-sdk/src/io.rs | 13 ++++++ engine/src/engine.rs | 10 +++++ engine/src/lib.rs | 49 +++++++++++++++++++++ 6 files changed, 189 insertions(+) create mode 100644 XCC.md create mode 100644 engine-precompiles/src/xcc.rs diff --git a/XCC.md b/XCC.md new file mode 100644 index 000000000..160a95f56 --- /dev/null +++ b/XCC.md @@ -0,0 +1,27 @@ +# Cross contract call + +## Engine interface + +- Propose: Any user can propose a new contract to be used as proxy contract. It is stored on-chain. +- Accept: Admin should accept some proposed contract. The current version is bumped! + +## Engine storage + +Maintain a list of all deployed contracts, with the version of the current deployed bytecode. Version is an integer bigger than 0. + +## Host function + +Whenever an updated is needed, a new proxy bytecode is proposed and accepted. Going forward before calling the proxy, +the new version will be deployed and initialized. + +Method init must be agnostic to the current state of the contract, it is possible updating a contract +skipping an arbitrary number of versions. + +### Promise anatomy + +When a call to xcc host function is made, a bundle is created using the concatenation of the following promises: + +- create-account (only if account hasn't been created yet) +- deploy-contract (only if current deployed contract doesn't match current version) +- init-call (only if deploy contrac must be called) +- xcc-access (alwasy) \ No newline at end of file diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index 10ab661e7..7cb9099d7 100644 --- a/engine-precompiles/src/lib.rs +++ b/engine-precompiles/src/lib.rs @@ -16,6 +16,7 @@ pub mod promise_result; pub mod random; pub mod secp256k1; mod utils; +pub mod xcc; use crate::account_ids::{predecessor_account, CurrentAccount, PredecessorAccount}; use crate::alt_bn256::{Bn256Add, Bn256Mul, Bn256Pair}; @@ -29,6 +30,7 @@ use crate::prelude::{Vec, H160, H256}; use crate::prepaid_gas::PrepaidGas; use crate::random::RandomSeed; use crate::secp256k1::ECRecover; +use crate::xcc::CrossContractCall; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::IO; use aurora_engine_sdk::promise::ReadOnlyPromiseHandler; @@ -103,6 +105,7 @@ pub struct Precompiles<'a, I, E, H> { // with the lifetime requirements on the type parameter `I`. pub near_exit: ExitToNear, pub ethereum_exit: ExitToEthereum, + pub cross_contract_call: CrossContractCall, pub predecessor_account_id: PredecessorAccount<'a, E>, pub prepaid_gas: PrepaidGas<'a, E>, pub promise_results: PromiseResult, @@ -280,6 +283,7 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, ) -> Self { let near_exit = ExitToNear::new(ctx.current_account_id.clone(), ctx.io); let ethereum_exit = ExitToEthereum::new(ctx.current_account_id, ctx.io); + let cross_contract_call = CrossContractCall::new(ctx.io); let predecessor_account_id = PredecessorAccount::new(ctx.env); let prepaid_gas = PrepaidGas::new(ctx.env); let promise_results = PromiseResult::new(ctx.promise_handler); @@ -288,6 +292,7 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, generic_precompiles, near_exit, ethereum_exit, + cross_contract_call, predecessor_account_id, prepaid_gas, promise_results, @@ -303,6 +308,8 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, return Some(f(&self.near_exit)); } else if address == exit_to_ethereum::ADDRESS { return Some(f(&self.ethereum_exit)); + } else if address == xcc::cross_contract_call::ADDRESS { + return Some(f(&self.cross_contract_call)); } else if address == predecessor_account::ADDRESS { return Some(f(&self.predecessor_account_id)); } else if address == prepaid_gas::ADDRESS { diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs new file mode 100644 index 000000000..7a62b9fa1 --- /dev/null +++ b/engine-precompiles/src/xcc.rs @@ -0,0 +1,83 @@ +//! Cross contract call precompile. +//! +//! Allow Aurora users interacting with NEAR smart contracts using cross contract call primitives. +//! TODO: How they work (low level explanation with examples) + +use crate::{Context, EvmPrecompileResult, Precompile, PrecompileOutput}; +use aurora_engine_sdk::io::IO; +use aurora_engine_types::types::EthGas; +use evm_core::ExitError; +use std::borrow::Cow; + +mod costs { + use crate::prelude::types::{EthGas, NearGas}; + + // TODO(#483): Determine the correct amount of gas + pub(super) const CROSS_CONTRACT_CALL: EthGas = EthGas::new(0); +} + +pub struct CrossContractCall { + io: I, +} + +impl CrossContractCall { + pub fn new(io: I) -> Self { + Self { io } + } +} + +pub mod cross_contract_call { + use aurora_engine_types::types::Address; + + /// Exit to Ethereum precompile address + /// + /// Address: `0x516cded1d16af10cad47d6d49128e2eb7d27b372` + /// This address is computed as: `&keccak("nearCrossContractCall")[12..]` + pub const ADDRESS: Address = + crate::make_address(0x516cded1, 0xd16af10cad47d6d49128e2eb7d27b372); +} + +impl Precompile for CrossContractCall { + fn required_gas(input: &[u8]) -> Result { + Ok(costs::CROSS_CONTRACT_CALL) + } + + fn run( + &self, + input: &[u8], + target_gas: Option, + context: &Context, + is_static: bool, + ) -> EvmPrecompileResult { + if let Some(target_gas) = target_gas { + if Self::required_gas(input)? > target_gas { + return Err(ExitError::OutOfGas); + } + } + + // It's not allowed to call cross contract call precompile in static mode + if is_static { + return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_STATIC"))); + } + + Ok(PrecompileOutput { + logs: vec![], + ..Default::default() + } + .into()) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::sdk::types::near_account_to_evm_address; + use crate::xcc::cross_contract_call; + + #[test] + fn test_precompile_id() { + assert_eq!( + cross_contract_call::ADDRESS, + near_account_to_evm_address("nearCrossContractCall".as_bytes()) + ); + } +} diff --git a/engine-sdk/src/io.rs b/engine-sdk/src/io.rs index 05642d45f..4916c72c4 100644 --- a/engine-sdk/src/io.rs +++ b/engine-sdk/src/io.rs @@ -88,6 +88,19 @@ pub trait IO { Ok(buf) } + /// Convenience function to read the input into a 32-byte array. + fn read_input_arr32(&self) -> Result<[u8; 32], error::IncorrectInputLength> { + let value = self.read_input(); + + if value.len() != 32 { + return Err(error::IncorrectInputLength); + } + + let mut buf = [0u8; 32]; + value.copy_to_slice(&mut buf); + Ok(buf) + } + /// Convenience function to store the input directly in storage under the /// given key (without ever loading it into memory). fn read_input_and_store(&mut self, key: &[u8]) { diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 1b9ad86c1..f36262802 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -16,6 +16,7 @@ use crate::accounting; use crate::parameters::{DeployErc20TokenArgs, NewCallArgs, TransactionStatus}; 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::transactions::{EthTransactionKind, NormalizedEthTransaction}; use crate::prelude::{ @@ -684,6 +685,14 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { status.into_result(result) } + pub fn factory_propose(&mut self, code: Vec) -> H256 { + todo!(); // Store proposal in state. Return hash of the proposal. + } + + pub fn factory_accept(&mut self, hash: H256) -> bool { + todo!(); // Find proposal, check it can only be called by admin. Update default contract and bump version. + } + fn relayer_key(account_id: &[u8]) -> Vec { bytes_to_key(KeyPrefix::RelayerEvmAddressMap, account_id) } @@ -1365,6 +1374,7 @@ where .filter_map(|log| { if log.address == exit_to_near::ADDRESS.raw() || log.address == exit_to_ethereum::ADDRESS.raw() + || log.address == cross_contract_call::ADDRESS.raw() { if log.topics.is_empty() { if let Ok(promise) = PromiseArgs::try_from_slice(&log.data) { diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 607d79e7f..d66a7ebec 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -282,6 +282,55 @@ mod contract { ); } + /// Propose new contract bytecode for proxy contracts. + /// + /// Anyone can submit a proposal. The user submitting the proposal should cover the storage + /// staking fee. The proposal is used when the admin invokes `factory_accept` using + /// the hash (sha256) of this proposal as an argument. + #[no_mangle] + pub extern "C" fn factory_propose() { + let io = Runtime; + let proxy_bytecode: Vec = io.read_input_borsh().sdk_unwrap(); + + let current_account_id = io.current_account_id(); + let predecessor_account_id = io.predecessor_account_id(); + let mut engine = Engine::new( + predecessor_address(&predecessor_account_id), + current_account_id, + io, + &io, + ) + .sdk_unwrap(); + + engine.factory_propose(proxy_bytecode); + } + + /// Accept proposed contract bytecode as the proxy contract. + /// + /// After a contract is accepted all proxies will be migrated to the new version before being + /// used. This process will happen lazy on demand (usage). Only `aurora` admin can invoke + /// this method. + #[no_mangle] + pub extern "C" fn factory_accept() { + let io = Runtime; + let proxy_bytecode_hash = io.read_input_arr32().sdk_unwrap(); + + let mut state = engine::get_state(&io).sdk_unwrap(); + let predecessor_account_id = io.predecessor_account_id(); + require_owner_only(&state, &predecessor_account_id); + + let current_account_id = io.current_account_id(); + let mut engine = Engine::new( + predecessor_address(&predecessor_account_id), + current_account_id, + io, + &io, + ) + .sdk_unwrap(); + + engine.factory_accept(H256(proxy_bytecode_hash)); + } + /// Allow receiving NEP141 tokens to the EVM contract. /// /// This function returns the amount of tokens to return to the sender. From 0f0092e77ff04754a4600083a3ddeee3c2fb8d12 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 27 Jul 2022 16:22:56 +0200 Subject: [PATCH 02/30] Fix compilation --- engine-precompiles/src/xcc.rs | 11 ++++++----- engine/src/engine.rs | 4 ++-- engine/src/lib.rs | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 7a62b9fa1..29b77f64b 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -5,12 +5,11 @@ use crate::{Context, EvmPrecompileResult, Precompile, PrecompileOutput}; use aurora_engine_sdk::io::IO; -use aurora_engine_types::types::EthGas; +use aurora_engine_types::{types::EthGas, vec, Cow}; use evm_core::ExitError; -use std::borrow::Cow; mod costs { - use crate::prelude::types::{EthGas, NearGas}; + use crate::prelude::types::EthGas; // TODO(#483): Determine the correct amount of gas pub(super) const CROSS_CONTRACT_CALL: EthGas = EthGas::new(0); @@ -38,7 +37,7 @@ pub mod cross_contract_call { } impl Precompile for CrossContractCall { - fn required_gas(input: &[u8]) -> Result { + fn required_gas(_input: &[u8]) -> Result { Ok(costs::CROSS_CONTRACT_CALL) } @@ -55,9 +54,11 @@ impl Precompile for CrossContractCall { } } - // It's not allowed to call cross contract call precompile in static mode + // It's not allowed to call cross contract call precompile in static or delegate mode if is_static { return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_STATIC"))); + } else if context.address != cross_contract_call::ADDRESS.raw() { + return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_DELEGATE"))); } Ok(PrecompileOutput { diff --git a/engine/src/engine.rs b/engine/src/engine.rs index f36262802..24390c86b 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -685,11 +685,11 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { status.into_result(result) } - pub fn factory_propose(&mut self, code: Vec) -> H256 { + pub fn factory_propose(&mut self, _code: Vec) -> H256 { todo!(); // Store proposal in state. Return hash of the proposal. } - pub fn factory_accept(&mut self, hash: H256) -> bool { + pub fn factory_accept(&mut self, _hash: H256) -> bool { todo!(); // Find proposal, check it can only be called by admin. Update default contract and bump version. } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index d66a7ebec..80d622c63 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -94,6 +94,7 @@ mod contract { use aurora_engine_sdk::io::{StorageIntermediate, IO}; use aurora_engine_sdk::near_runtime::{Runtime, ViewEnv}; use aurora_engine_sdk::promise::PromiseHandler; + use aurora_engine_types::Vec; #[cfg(feature = "integration-test")] use crate::prelude::NearGas; @@ -315,7 +316,7 @@ mod contract { let io = Runtime; let proxy_bytecode_hash = io.read_input_arr32().sdk_unwrap(); - let mut state = engine::get_state(&io).sdk_unwrap(); + let state = engine::get_state(&io).sdk_unwrap(); let predecessor_account_id = io.predecessor_account_id(); require_owner_only(&state, &predecessor_account_id); From 169fb9e078d775378ed43476daaa36c54befa684 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 27 Jul 2022 17:43:37 +0200 Subject: [PATCH 03/30] Add some implementation details to the cross contract call precompile --- engine-precompiles/src/lib.rs | 4 +- engine-precompiles/src/xcc.rs | 85 +++++++++++++++++++++++++++++++--- engine-types/src/parameters.rs | 15 ++++++ engine-types/src/types/gas.rs | 8 ++++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index 7cb9099d7..4a406debe 100644 --- a/engine-precompiles/src/lib.rs +++ b/engine-precompiles/src/lib.rs @@ -282,8 +282,8 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, ctx: PrecompileConstructorContext<'a, I, E, H>, ) -> Self { let near_exit = ExitToNear::new(ctx.current_account_id.clone(), ctx.io); - let ethereum_exit = ExitToEthereum::new(ctx.current_account_id, ctx.io); - let cross_contract_call = CrossContractCall::new(ctx.io); + let ethereum_exit = ExitToEthereum::new(ctx.current_account_id.clone(), ctx.io); + let cross_contract_call = CrossContractCall::new(ctx.current_account_id, ctx.io); let predecessor_account_id = PredecessorAccount::new(ctx.env); let prepaid_gas = PrepaidGas::new(ctx.env); let promise_results = PromiseResult::new(ctx.promise_handler); diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 29b77f64b..dc99fd24c 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -5,23 +5,46 @@ use crate::{Context, EvmPrecompileResult, Precompile, PrecompileOutput}; use aurora_engine_sdk::io::IO; -use aurora_engine_types::{types::EthGas, vec, Cow}; +use aurora_engine_types::{ + account_id::AccountId, + format, + parameters::{CrossContractCallArgs, PromiseArgs, PromiseCreateArgs}, + types::{balance::ZERO_YOCTO, EthGas}, + vec, Cow, Vec, H160, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use evm::backend::Log; use evm_core::ExitError; +const ERR_INVALID_INPUT: &str = "ERR_INVALID_XCC_INPUT"; +const ERR_SERIALIZE: &str = "ERR_XCC_CALL_SERIALIZE"; +const ERR_STATIC: &str = "ERR_INVALID_IN_STATIC"; +const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE"; +const ROUTER_EXEC_NAME: &str = "execute"; +const ROUTER_SCHEDULE_NAME: &str = "schedule"; + mod costs { - use crate::prelude::types::EthGas; + use crate::prelude::types::{EthGas, NearGas}; // TODO(#483): Determine the correct amount of gas pub(super) const CROSS_CONTRACT_CALL: EthGas = EthGas::new(0); + + // TODO: Determine the correct amount of gas + pub(super) const ROUTER_EXEC: NearGas = NearGas::new(0); + pub(super) const ROUTER_SCHEDULE: NearGas = NearGas::new(0); } pub struct CrossContractCall { io: I, + engine_account_id: AccountId, } impl CrossContractCall { - pub fn new(io: I) -> Self { - Self { io } + pub fn new(engine_account_id: AccountId, io: I) -> Self { + Self { + io, + engine_account_id, + } } } @@ -56,19 +79,67 @@ impl Precompile for CrossContractCall { // It's not allowed to call cross contract call precompile in static or delegate mode if is_static { - return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_STATIC"))); + return Err(ExitError::Other(Cow::from(ERR_STATIC))); } else if context.address != cross_contract_call::ADDRESS.raw() { - return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_DELEGATE"))); + return Err(ExitError::Other(Cow::from(ERR_DELEGATE))); } + let sender = context.caller; + let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref()); + // TODO: Is it ok to use Borsh to read the input? It might not be very friendly to construct the input in Solidity... + let args = CrossContractCallArgs::try_from_slice(input) + .map_err(|_| ExitError::Other(Cow::from(ERR_INVALID_INPUT)))?; + let promise = match args { + CrossContractCallArgs::Eager(call) => { + let call_gas = match &call { + PromiseArgs::Create(call) => call.attached_gas, + PromiseArgs::Callback(cb) => cb.base.attached_gas + cb.callback.attached_gas, + }; + PromiseCreateArgs { + target_account_id, + method: ROUTER_EXEC_NAME.into(), + args: call + .try_to_vec() + .map_err(|_| ExitError::Other(Cow::from(ERR_SERIALIZE)))?, + attached_balance: ZERO_YOCTO, + attached_gas: costs::ROUTER_EXEC + call_gas, + } + } + CrossContractCallArgs::Delayed(call) => PromiseCreateArgs { + target_account_id, + method: ROUTER_SCHEDULE_NAME.into(), + args: call + .try_to_vec() + .map_err(|_| ExitError::Other(Cow::from(ERR_SERIALIZE)))?, + attached_balance: ZERO_YOCTO, + // We don't need to add any gas to the amount need for the schedule call + // since the promise is not executed right away. + attached_gas: costs::ROUTER_SCHEDULE, + }, + }; + + let promise_log = Log { + address: cross_contract_call::ADDRESS.raw(), + topics: Vec::new(), + data: promise + .try_to_vec() + .map_err(|_| ExitError::Other(Cow::from(ERR_SERIALIZE)))?, + }; + Ok(PrecompileOutput { - logs: vec![], + logs: vec![promise_log], ..Default::default() } .into()) } } +fn create_target_account_id(sender: H160, engine_account_id: &str) -> AccountId { + format!("{}.{}", hex::encode(sender.as_bytes()), engine_account_id) + .parse() + .unwrap() +} + #[cfg(test)] mod tests { use crate::prelude::sdk::types::near_account_to_evm_address; diff --git a/engine-types/src/parameters.rs b/engine-types/src/parameters.rs index 82ea30dcd..0ebd0ef19 100644 --- a/engine-types/src/parameters.rs +++ b/engine-types/src/parameters.rs @@ -64,3 +64,18 @@ pub struct RefundCallArgs { pub erc20_address: Option
, pub amount: RawU256, } + +/// Args passed to the the cross contract call precompile. +/// That precompile is used by Aurora contracts to make calls to the broader NEAR ecosystem. +/// See https://github.com/aurora-is-near/AIPs/pull/2 for design details. +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub enum CrossContractCallArgs { + /// The promise is to be executed immediately (as part of the same NEAR transaction as the EVM call). + Eager(PromiseArgs), + /// The promise is to be stored in the router contract, and can be executed in a future transaction. + /// The purpose of this is to expand how much NEAR gas can be made available to a cross contract call. + /// For example, if an expensive EVM call ends with a NEAR cross contract call, then there may not be + /// much gas left to perform it. In this case, the promise could be `Delayed` (stored in the router) + /// and executed in a separate transaction with a fresh 300 Tgas available for it. + Delayed(PromiseArgs), +} diff --git a/engine-types/src/types/gas.rs b/engine-types/src/types/gas.rs index f5f36a5e4..0232b776d 100644 --- a/engine-types/src/types/gas.rs +++ b/engine-types/src/types/gas.rs @@ -24,6 +24,14 @@ impl Sub for NearGas { } } +impl Add for NearGas { + type Output = NearGas; + + fn add(self, rhs: NearGas) -> Self::Output { + Self(self.0 + rhs.0) + } +} + impl NearGas { /// Constructs a new `NearGas` with a given u64 value. pub const fn new(gas: u64) -> NearGas { From 406da680efcd42da7f5954d4660627dec42ba628 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Thu, 28 Jul 2022 19:25:38 +0200 Subject: [PATCH 04/30] Functions for reading/writing cross-contract data from/to storage --- engine-sdk/src/error.rs | 15 ++++++++ engine-sdk/src/io.rs | 16 ++++++++ engine-types/src/storage.rs | 2 + engine/src/engine.rs | 8 ---- engine/src/lib.rs | 56 +++++---------------------- engine/src/xcc.rs | 76 +++++++++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 55 deletions(-) create mode 100644 engine/src/xcc.rs diff --git a/engine-sdk/src/error.rs b/engine-sdk/src/error.rs index 5e4cffa37..87c61610f 100644 --- a/engine-sdk/src/error.rs +++ b/engine-sdk/src/error.rs @@ -16,6 +16,21 @@ impl AsRef<[u8]> for IncorrectInputLength { } } +#[derive(Debug)] +pub enum ReadU32Error { + InvalidU32, + MissingValue, +} + +impl AsRef<[u8]> for ReadU32Error { + fn as_ref(&self) -> &[u8] { + match self { + Self::InvalidU32 => b"ERR_NOT_U32", + Self::MissingValue => b"ERR_U32_NOT_FOUND", + } + } +} + #[derive(Debug)] pub enum ReadU64Error { InvalidU64, diff --git a/engine-sdk/src/io.rs b/engine-sdk/src/io.rs index 4916c72c4..7aeb7aaa8 100644 --- a/engine-sdk/src/io.rs +++ b/engine-sdk/src/io.rs @@ -108,6 +108,22 @@ pub trait IO { self.write_storage_direct(key, value); } + /// Convenience function to read a 32-bit unsigned integer from storage + /// (assumes little-endian encoding). + fn read_u32(&self, key: &[u8]) -> Result { + let value = self + .read_storage(key) + .ok_or(error::ReadU32Error::MissingValue)?; + + if value.len() != 4 { + return Err(error::ReadU32Error::InvalidU32); + } + + let mut result = [0u8; 4]; + value.copy_to_slice(&mut result); + Ok(u32::from_le_bytes(result)) + } + /// Convenience function to read a 64-bit unsigned integer from storage /// (assumes little-endian encoding). fn read_u64(&self, key: &[u8]) -> Result { diff --git a/engine-types/src/storage.rs b/engine-types/src/storage.rs index a3a3738e8..294357983 100644 --- a/engine-types/src/storage.rs +++ b/engine-types/src/storage.rs @@ -29,6 +29,7 @@ pub enum KeyPrefix { Generation = 0x7, Nep141Erc20Map = 0x8, Erc20Nep141Map = 0x9, + CrossContractCall = 0xa, } impl From for u8 { @@ -91,6 +92,7 @@ impl From for KeyPrefix { 0x7 => Self::Generation, 0x8 => Self::Nep141Erc20Map, 0x9 => Self::Erc20Nep141Map, + 0xa => Self::CrossContractCall, _ => unreachable!(), } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 24390c86b..925c7b73a 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -685,14 +685,6 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { status.into_result(result) } - pub fn factory_propose(&mut self, _code: Vec) -> H256 { - todo!(); // Store proposal in state. Return hash of the proposal. - } - - pub fn factory_accept(&mut self, _hash: H256) -> bool { - todo!(); // Find proposal, check it can only be called by admin. Update default contract and bump version. - } - fn relayer_key(account_id: &[u8]) -> Vec { bytes_to_key(KeyPrefix::RelayerEvmAddressMap, account_id) } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 80d622c63..446b66a12 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -28,6 +28,7 @@ pub mod fungible_token; pub mod json; pub mod log_entry; mod prelude; +pub mod xcc; #[cfg(target_arch = "wasm32")] #[global_allocator] @@ -94,7 +95,6 @@ mod contract { use aurora_engine_sdk::io::{StorageIntermediate, IO}; use aurora_engine_sdk::near_runtime::{Runtime, ViewEnv}; use aurora_engine_sdk::promise::PromiseHandler; - use aurora_engine_types::Vec; #[cfg(feature = "integration-test")] use crate::prelude::NearGas; @@ -283,53 +283,15 @@ mod contract { ); } - /// Propose new contract bytecode for proxy contracts. - /// - /// Anyone can submit a proposal. The user submitting the proposal should cover the storage - /// staking fee. The proposal is used when the admin invokes `factory_accept` using - /// the hash (sha256) of this proposal as an argument. - #[no_mangle] - pub extern "C" fn factory_propose() { - let io = Runtime; - let proxy_bytecode: Vec = io.read_input_borsh().sdk_unwrap(); - - let current_account_id = io.current_account_id(); - let predecessor_account_id = io.predecessor_account_id(); - let mut engine = Engine::new( - predecessor_address(&predecessor_account_id), - current_account_id, - io, - &io, - ) - .sdk_unwrap(); - - engine.factory_propose(proxy_bytecode); - } - - /// Accept proposed contract bytecode as the proxy contract. - /// - /// After a contract is accepted all proxies will be migrated to the new version before being - /// used. This process will happen lazy on demand (usage). Only `aurora` admin can invoke - /// this method. + /// Updates the bytecode for user's router contracts created by the engine. + /// These contracts are where cross-contract calls initiated by the EVM precompile + /// will be sent from. #[no_mangle] - pub extern "C" fn factory_accept() { - let io = Runtime; - let proxy_bytecode_hash = io.read_input_arr32().sdk_unwrap(); - - let state = engine::get_state(&io).sdk_unwrap(); - let predecessor_account_id = io.predecessor_account_id(); - require_owner_only(&state, &predecessor_account_id); - - let current_account_id = io.current_account_id(); - let mut engine = Engine::new( - predecessor_address(&predecessor_account_id), - current_account_id, - io, - &io, - ) - .sdk_unwrap(); - - engine.factory_accept(H256(proxy_bytecode_hash)); + pub extern "C" fn factory_update() { + let mut io = Runtime; + let bytes = io.read_input().to_vec(); + let router_bytecode = crate::xcc::RouterCode(bytes); + crate::xcc::update_router_code(&mut io, &router_bytecode); } /// Allow receiving NEP141 tokens to the EVM contract. diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs new file mode 100644 index 000000000..35e5bf579 --- /dev/null +++ b/engine/src/xcc.rs @@ -0,0 +1,76 @@ +use aurora_engine_sdk::error::ReadU32Error; +use aurora_engine_sdk::io::{StorageIntermediate, IO}; +use aurora_engine_types::storage::{self, KeyPrefix}; +use aurora_engine_types::types::Address; +use aurora_engine_types::Vec; + +pub const ERR_NO_ROUTER_CODE: &str = "ERR_MISSING_XCC_BYTECODE"; +pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; +pub const VERSION_KEY: &[u8] = b"version"; +pub const CODE_KEY: &[u8] = b"router_code"; + +/// Type wrapper for version of router contracts. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CodeVersion(pub u32); + +impl CodeVersion { + pub fn increment(self) -> Self { + Self(self.0 + 1) + } +} + +/// Type wrapper for router bytecode. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RouterCode(pub Vec); + +/// Read the current wasm bytecode for the router contracts +pub fn get_router_code(io: &I) -> RouterCode { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, CODE_KEY); + let bytes = io.read_storage(&key).expect(ERR_NO_ROUTER_CODE).to_vec(); + RouterCode(bytes) +} + +/// Set new router bytecode, and update increment the version by 1. +pub fn update_router_code(io: &mut I, code: &RouterCode) { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, CODE_KEY); + io.write_storage(&key, &code.0); + + let current_version = get_latest_code_version(io); + set_latest_code_version(io, current_version.increment()); +} + +/// Get the latest router contract version. +pub fn get_latest_code_version(io: &I) -> CodeVersion { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, VERSION_KEY); + read_version(io, &key).unwrap_or_default() +} + +/// Get the version of the currently deploy router for the given address (if it exists). +pub fn get_code_version_of_address(io: &I, address: &Address) -> Option { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, address.as_bytes()); + read_version(io, &key) +} + +/// Set the version of the router contract deployed for the given address. +pub fn set_code_version_of_address(io: &mut I, address: &Address, version: CodeVersion) { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, address.as_bytes()); + let value_bytes = version.0.to_le_bytes(); + io.write_storage(&key, &value_bytes); +} + +/// Sets the latest router contract version. This function is intentionally private because +/// it should never be set manually. The version is managed automatically by `update_router_code`. +fn set_latest_code_version(io: &mut I, version: CodeVersion) { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, VERSION_KEY); + let value_bytes = version.0.to_le_bytes(); + io.write_storage(&key, &value_bytes); +} + +/// Private utility method for reading code version from storage. +fn read_version(io: &I, key: &[u8]) -> Option { + match io.read_u32(key) { + Ok(value) => Some(CodeVersion(value)), + Err(ReadU32Error::MissingValue) => None, + Err(ReadU32Error::InvalidU32) => panic!("{}", ERR_CORRUPTED_STORAGE), + } +} From 7410ccf7eb22dc7747bfdb4f5ac8c7c6bb065f42 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Thu, 28 Jul 2022 20:15:04 +0200 Subject: [PATCH 05/30] Outline handling of xcc promises --- engine-sdk/src/near_runtime.rs | 5 ++- engine-types/src/parameters.rs | 1 + engine/src/engine.rs | 19 +++++++-- engine/src/xcc.rs | 72 +++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/engine-sdk/src/near_runtime.rs b/engine-sdk/src/near_runtime.rs index 20844dffe..a62640f75 100644 --- a/engine-sdk/src/near_runtime.rs +++ b/engine-sdk/src/near_runtime.rs @@ -339,6 +339,9 @@ impl crate::promise::PromiseHandler for Runtime { for action in args.actions.iter() { match action { + PromiseAction::CreateAccount => unsafe { + exports::promise_batch_action_create_account(id); + }, PromiseAction::Transfer { amount } => unsafe { let amount = amount.as_u128(); exports::promise_batch_action_transfer(id, &amount as *const u128 as _); @@ -520,7 +523,7 @@ pub(crate) mod exports { // ####################### // # Promise API actions # // ####################### - fn promise_batch_action_create_account(promise_index: u64); + pub(crate) fn promise_batch_action_create_account(promise_index: u64); pub(crate) fn promise_batch_action_deploy_contract( promise_index: u64, code_len: u64, diff --git a/engine-types/src/parameters.rs b/engine-types/src/parameters.rs index 0ebd0ef19..8a07bde19 100644 --- a/engine-types/src/parameters.rs +++ b/engine-types/src/parameters.rs @@ -29,6 +29,7 @@ pub struct PromiseWithCallbackArgs { #[derive(Debug, BorshSerialize, BorshDeserialize, Clone)] pub enum PromiseAction { + CreateAccount, Transfer { amount: Yocto, }, diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 925c7b73a..fdabbb32a 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -555,7 +555,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { }; let (values, logs) = executor.into_state().deconstruct(); - let logs = filter_promises_from_logs(handler, logs); + let logs = filter_promises_from_logs(&mut self.io, handler, logs, &self.current_account_id); self.apply(values, Vec::::new(), true); @@ -640,7 +640,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { }; let (values, logs) = executor.into_state().deconstruct(); - let logs = filter_promises_from_logs(handler, logs); + let logs = filter_promises_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. @@ -1357,16 +1357,21 @@ fn remove_account(io: &mut I, address: &Address, generation: u32) remove_all_storage(io, address, generation); } -fn filter_promises_from_logs(handler: &mut P, logs: T) -> Vec +fn filter_promises_from_logs( + io: &mut I, + handler: &mut P, + logs: T, + current_account_id: &AccountId, +) -> Vec where T: IntoIterator, P: PromiseHandler, + I: IO, { logs.into_iter() .filter_map(|log| { if log.address == exit_to_near::ADDRESS.raw() || log.address == exit_to_ethereum::ADDRESS.raw() - || log.address == cross_contract_call::ADDRESS.raw() { if log.topics.is_empty() { if let Ok(promise) = PromiseArgs::try_from_slice(&log.data) { @@ -1386,6 +1391,12 @@ where // `topics` field. Some(log.into()) } + } else if log.address == cross_contract_call::ADDRESS.raw() { + if let Ok(promise) = PromiseCreateArgs::try_from_slice(&log.data) { + crate::xcc::handle_precomile_promise(io, handler, promise, current_account_id); + } + // do not pass on these "internal logs" to caller + None } else { Some(log.into()) } diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index 35e5bf579..b1bc3ffbb 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -1,16 +1,22 @@ use aurora_engine_sdk::error::ReadU32Error; use aurora_engine_sdk::io::{StorageIntermediate, IO}; +use aurora_engine_sdk::promise::PromiseHandler; +use aurora_engine_types::account_id::AccountId; +use aurora_engine_types::parameters::{PromiseAction, PromiseBatchAction, PromiseCreateArgs}; use aurora_engine_types::storage::{self, KeyPrefix}; -use aurora_engine_types::types::Address; +use aurora_engine_types::types::{Address, NearGas, ZERO_YOCTO}; use aurora_engine_types::Vec; pub const ERR_NO_ROUTER_CODE: &str = "ERR_MISSING_XCC_BYTECODE"; pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; +pub const ERR_INVALID_ACCOUNT: &str = "ERR_INVALID_XCC_ACCOUNT"; pub const VERSION_KEY: &[u8] = b"version"; pub const CODE_KEY: &[u8] = b"router_code"; +// TODO: estimate gas +pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(0); /// Type wrapper for version of router contracts. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct CodeVersion(pub u32); impl CodeVersion { @@ -23,6 +29,68 @@ impl CodeVersion { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RouterCode(pub Vec); +pub fn handle_precomile_promise( + io: &mut I, + handler: &mut P, + promise: PromiseCreateArgs, + current_account_id: &AccountId, +) where + P: PromiseHandler, + I: IO, +{ + let target_account: &str = promise.target_account_id.as_ref(); + let sender = Address::decode(&target_account[0..40]).expect(ERR_INVALID_ACCOUNT); + let latest_code_version = get_latest_code_version(io); + let sender_code_version = get_code_version_of_address(io, &sender); + let mut promise_actions = Vec::new(); + let mut deploy_needed = false; + match sender_code_version { + None => { + // Need to create the account + promise_actions.push(PromiseAction::CreateAccount); + // Then deploy the contract + promise_actions.push(PromiseAction::DeployConotract { + code: get_router_code(io).0, + }); + deploy_needed = true; + } + Some(version) if version < latest_code_version => { + // Account exist, but with outdated version, so deploy new one + promise_actions.push(PromiseAction::DeployConotract { + code: get_router_code(io).0, + }); + deploy_needed = true; + } + Some(_version) => { + // if the version match then we do not need to deploy, it already up to date + } + }; + // Regardless of whether a deploy is needed or not, we want to make a call to the account + promise_actions.push(PromiseAction::FunctionCall { + name: promise.method, + args: promise.args, + attached_yocto: promise.attached_balance, + gas: promise.attached_gas, + }); + let batch = PromiseBatchAction { + target_account_id: promise.target_account_id, + actions: promise_actions, + }; + let promise_id = handler.promise_create_batch(&batch); + if deploy_needed { + // If a deploy was needed then we want there to be a callback here to update the version of the account + let callback = PromiseCreateArgs { + target_account_id: current_account_id.clone(), + method: "factory_update_account_version".into(), + args: Vec::new(), // TODO + attached_balance: ZERO_YOCTO, + attached_gas: VERSION_UPDATE_GAS, + }; + + handler.promise_attach_callback(promise_id, &callback); + } +} + /// Read the current wasm bytecode for the router contracts pub fn get_router_code(io: &I) -> RouterCode { let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, CODE_KEY); From 865027115403ebb6e8d1e190ca5ad8e031a71f3f Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Thu, 28 Jul 2022 20:49:11 +0200 Subject: [PATCH 06/30] Clean up xcc promise handling; add factory_update_address_version function --- engine/src/engine.rs | 6 +++--- engine/src/lib.rs | 12 ++++++++++++ engine/src/xcc.rs | 21 +++++++++++++++++---- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/engine/src/engine.rs b/engine/src/engine.rs index fdabbb32a..278524d9c 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -555,7 +555,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { }; let (values, logs) = executor.into_state().deconstruct(); - let logs = filter_promises_from_logs(&mut self.io, handler, logs, &self.current_account_id); + let logs = filter_promises_from_logs(&self.io, handler, logs, &self.current_account_id); self.apply(values, Vec::::new(), true); @@ -640,7 +640,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { }; let (values, logs) = executor.into_state().deconstruct(); - let logs = filter_promises_from_logs(&mut self.io, handler, logs, &self.current_account_id); + let logs = filter_promises_from_logs(&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. @@ -1358,7 +1358,7 @@ fn remove_account(io: &mut I, address: &Address, generation: u32) } fn filter_promises_from_logs( - io: &mut I, + io: &I, handler: &mut P, logs: T, current_account_id: &AccountId, diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 446b66a12..1dca7b59d 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -289,11 +289,23 @@ mod contract { #[no_mangle] pub extern "C" fn factory_update() { let mut io = Runtime; + let state = engine::get_state(&io).sdk_unwrap(); + require_owner_only(&state, &io.predecessor_account_id()); let bytes = io.read_input().to_vec(); let router_bytecode = crate::xcc::RouterCode(bytes); crate::xcc::update_router_code(&mut io, &router_bytecode); } + /// Updates the bytecode version for the given account. This is only called as a callback + /// when a new version of the router contract is deployed to an account. + #[no_mangle] + pub extern "C" fn factory_update_address_version() { + let mut io = Runtime; + io.assert_private_call().sdk_unwrap(); + let args: crate::xcc::AddressVersionUpdateArgs = io.read_input_borsh().sdk_unwrap(); + crate::xcc::set_code_version_of_address(&mut io, &args.address, args.version); + } + /// Allow receiving NEP141 tokens to the EVM contract. /// /// This function returns the amount of tokens to return to the sender. diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index b1bc3ffbb..9ee062bc1 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -6,6 +6,7 @@ use aurora_engine_types::parameters::{PromiseAction, PromiseBatchAction, Promise use aurora_engine_types::storage::{self, KeyPrefix}; use aurora_engine_types::types::{Address, NearGas, ZERO_YOCTO}; use aurora_engine_types::Vec; +use borsh::{BorshDeserialize, BorshSerialize}; pub const ERR_NO_ROUTER_CODE: &str = "ERR_MISSING_XCC_BYTECODE"; pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; @@ -16,7 +17,9 @@ pub const CODE_KEY: &[u8] = b"router_code"; pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(0); /// Type wrapper for version of router contracts. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, BorshDeserialize, BorshSerialize, +)] pub struct CodeVersion(pub u32); impl CodeVersion { @@ -29,8 +32,14 @@ impl CodeVersion { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RouterCode(pub Vec); +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub struct AddressVersionUpdateArgs { + pub address: Address, + pub version: CodeVersion, +} + pub fn handle_precomile_promise( - io: &mut I, + io: &I, handler: &mut P, promise: PromiseCreateArgs, current_account_id: &AccountId, @@ -79,10 +88,14 @@ pub fn handle_precomile_promise( let promise_id = handler.promise_create_batch(&batch); if deploy_needed { // If a deploy was needed then we want there to be a callback here to update the version of the account + let args = AddressVersionUpdateArgs { + address: sender, + version: latest_code_version, + }; let callback = PromiseCreateArgs { target_account_id: current_account_id.clone(), - method: "factory_update_account_version".into(), - args: Vec::new(), // TODO + method: "factory_update_address_version".into(), + args: args.try_to_vec().unwrap(), attached_balance: ZERO_YOCTO, attached_gas: VERSION_UPDATE_GAS, }; From 6fe234dbffd907b27831389d11c236afb1985fd6 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Thu, 28 Jul 2022 21:23:33 +0200 Subject: [PATCH 07/30] Include call to initialize when new router contract code is deployed --- engine/src/xcc.rs | 102 ++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index 9ee062bc1..47e5f657c 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -15,6 +15,7 @@ pub const VERSION_KEY: &[u8] = b"version"; pub const CODE_KEY: &[u8] = b"router_code"; // TODO: estimate gas pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(0); +pub const INITIALIZE_GAS: NearGas = NearGas::new(0); /// Type wrapper for version of router contracts. #[derive( @@ -51,57 +52,62 @@ pub fn handle_precomile_promise( let sender = Address::decode(&target_account[0..40]).expect(ERR_INVALID_ACCOUNT); let latest_code_version = get_latest_code_version(io); let sender_code_version = get_code_version_of_address(io, &sender); - let mut promise_actions = Vec::new(); - let mut deploy_needed = false; - match sender_code_version { - None => { - // Need to create the account - promise_actions.push(PromiseAction::CreateAccount); - // Then deploy the contract + let deploy_needed = match sender_code_version { + None => AddressVersionStatus::DeployNeeded { + create_needed: true, + }, + Some(version) if version < latest_code_version => AddressVersionStatus::DeployNeeded { + create_needed: false, + }, + Some(_version) => AddressVersionStatus::UpToDate, + }; + let _promise_id = match deploy_needed { + AddressVersionStatus::DeployNeeded { create_needed } => { + let mut promise_actions = Vec::with_capacity(4); + if create_needed { + promise_actions.push(PromiseAction::CreateAccount); + } promise_actions.push(PromiseAction::DeployConotract { code: get_router_code(io).0, }); - deploy_needed = true; - } - Some(version) if version < latest_code_version => { - // Account exist, but with outdated version, so deploy new one - promise_actions.push(PromiseAction::DeployConotract { - code: get_router_code(io).0, + // After a deploy we call the contract's initialize function + promise_actions.push(PromiseAction::FunctionCall { + name: "initialize".into(), + // TODO: initialize args? + args: Vec::new(), + attached_yocto: ZERO_YOCTO, + gas: INITIALIZE_GAS, }); - deploy_needed = true; - } - Some(_version) => { - // if the version match then we do not need to deploy, it already up to date + // After the contract is deployed and initialized, we can call the method requested + promise_actions.push(PromiseAction::FunctionCall { + name: promise.method, + args: promise.args, + attached_yocto: promise.attached_balance, + gas: promise.attached_gas, + }); + let batch = PromiseBatchAction { + target_account_id: promise.target_account_id, + actions: promise_actions, + }; + let promise_id = handler.promise_create_batch(&batch); + + // Add a callback here to update the version of the account + let args = AddressVersionUpdateArgs { + address: sender, + version: latest_code_version, + }; + let callback = PromiseCreateArgs { + target_account_id: current_account_id.clone(), + method: "factory_update_address_version".into(), + args: args.try_to_vec().unwrap(), + attached_balance: ZERO_YOCTO, + attached_gas: VERSION_UPDATE_GAS, + }; + + handler.promise_attach_callback(promise_id, &callback) } + AddressVersionStatus::UpToDate => handler.promise_create_call(&promise), }; - // Regardless of whether a deploy is needed or not, we want to make a call to the account - promise_actions.push(PromiseAction::FunctionCall { - name: promise.method, - args: promise.args, - attached_yocto: promise.attached_balance, - gas: promise.attached_gas, - }); - let batch = PromiseBatchAction { - target_account_id: promise.target_account_id, - actions: promise_actions, - }; - let promise_id = handler.promise_create_batch(&batch); - if deploy_needed { - // If a deploy was needed then we want there to be a callback here to update the version of the account - let args = AddressVersionUpdateArgs { - address: sender, - version: latest_code_version, - }; - let callback = PromiseCreateArgs { - target_account_id: current_account_id.clone(), - method: "factory_update_address_version".into(), - args: args.try_to_vec().unwrap(), - attached_balance: ZERO_YOCTO, - attached_gas: VERSION_UPDATE_GAS, - }; - - handler.promise_attach_callback(promise_id, &callback); - } } /// Read the current wasm bytecode for the router contracts @@ -155,3 +161,9 @@ fn read_version(io: &I, key: &[u8]) -> Option { Err(ReadU32Error::InvalidU32) => panic!("{}", ERR_CORRUPTED_STORAGE), } } + +/// Private enum used for bookkeeping what actions are needed in the call to the router contract. +enum AddressVersionStatus { + UpToDate, + DeployNeeded { create_needed: bool }, +} From 607904453ec7a106d678a11eac0d2525a0147bc0 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Fri, 29 Jul 2022 17:30:44 +0200 Subject: [PATCH 08/30] Write cross contract call router contract --- Cargo.toml | 1 + etc/xcc-router/Cargo.lock | 756 ++++++++++++++++++++++++++++++++++++++ etc/xcc-router/Cargo.toml | 12 + etc/xcc-router/src/lib.rs | 148 ++++++++ 4 files changed, 917 insertions(+) create mode 100644 etc/xcc-router/Cargo.lock create mode 100644 etc/xcc-router/Cargo.toml create mode 100644 etc/xcc-router/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e7c5c1abf..56c244ec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,4 +44,5 @@ exclude = [ "etc/tests/ft-receiver", "etc/tests/benchmark-contract", "etc/tests/self-contained-5bEgfRQ", + "etc/xcc-router", ] diff --git a/etc/xcc-router/Cargo.lock b/etc/xcc-router/Cargo.lock new file mode 100644 index 000000000..fde33fb83 --- /dev/null +++ b/etc/xcc-router/Cargo.lock @@ -0,0 +1,756 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + +[[package]] +name = "aurora-engine-types" +version = "1.0.0" +dependencies = [ + "borsh", + "ethabi", + "hex", + "primitive-types", + "sha3", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "borsh" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" +dependencies = [ + "borsh-derive", + "hashbrown 0.9.1", +] + +[[package]] +name = "borsh-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ethabi" +version = "14.1.0" +source = "git+https://github.com/darwinia-network/ethabi?branch=xavier-no-std#09da0834d95f8b43377ca22d7b1c97a2401f4b0c" +dependencies = [ + "anyhow", + "ethereum-types", + "hex", + "sha3", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +dependencies = [ + "ethbloom", + "fixed-hash", + "primitive-types", + "uint", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "keccak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "near-primitives-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2b3fb5acf3a494aed4e848446ef2d6ebb47dbe91c681105d4d1786c2ee63e52" +dependencies = [ + "base64", + "borsh", + "bs58", + "derive_more", + "hex", + "lazy_static", + "num-rational", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "near-rpc-error-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa8dbf8437a28ac40fcb85859ab0d0b8385013935b000c7a51ae79631dd74d9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "near-rpc-error-macro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6111d713e90c7c551dee937f4a06cb9ea2672243455a4454cc7566387ba2d9" +dependencies = [ + "near-rpc-error-core", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "near-runtime-utils" +version = "4.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48d80c4ca1d4cf99bc16490e1e3d49826c150dfc4410ac498918e45c7d98e07" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "near-sdk" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7383e242d3e07bf0951e8589d6eebd7f18bb1c1fc5fbec3fad796041a6aebd1" +dependencies = [ + "base64", + "borsh", + "bs58", + "near-primitives-core", + "near-sdk-macros", + "near-vm-logic", + "serde", + "serde_json", + "wee_alloc", +] + +[[package]] +name = "near-sdk-core" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284a78d9eb8eda58330462fa0023a6d7014c941df1f0387095e7dfd1dc0f2bce" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "near-sdk-macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2037337438f97d1ce5f7c896cf229dc56dacd5c01142d1ef95a7d778cde6ce7d" +dependencies = [ + "near-sdk-core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "near-vm-errors" +version = "4.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e281d8730ed8cb0e3e69fb689acee6b93cdb43824cd69a8ffd7e1bfcbd1177d7" +dependencies = [ + "borsh", + "hex", + "near-rpc-error-macro", + "serde", +] + +[[package]] +name = "near-vm-logic" +version = "4.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11cb28a2d07f37680efdaf860f4c9802828c44fc50c08009e7884de75d982c5" +dependencies = [ + "base64", + "borsh", + "bs58", + "byteorder", + "near-primitives-core", + "near-runtime-utils", + "near-vm-errors", + "serde", + "sha2", + "sha3", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash", + "impl-rlp", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "semver" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" + +[[package]] +name = "serde" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xcc_router" +version = "1.0.0" +dependencies = [ + "aurora-engine-types", + "near-sdk", +] diff --git a/etc/xcc-router/Cargo.toml b/etc/xcc-router/Cargo.toml new file mode 100644 index 000000000..aded56b05 --- /dev/null +++ b/etc/xcc-router/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xcc_router" +version = "1.0.0" +authors = ["Aurora "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +aurora-engine-types = { path = "../../engine-types", default-features = false } +near-sdk = "3.1.0" diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs new file mode 100644 index 000000000..0af535fd8 --- /dev/null +++ b/etc/xcc-router/src/lib.rs @@ -0,0 +1,148 @@ +use aurora_engine_types::parameters::{PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::{LazyOption, LookupMap}; +use near_sdk::json_types::U64; +use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault, PromiseIndex}; + +const VERSION_PREFIX: &[u8] = &[0x00]; +const PARENT_PREFIX: &[u8] = &[0x01]; +const NONCE_PREFIX: &[u8] = &[0x02]; +const MAP_PREFIX: &[u8] = &[0x03]; + +const CURRENT_VERSION: u32 = 0; + +const ERR_ILLEGAL_CALLER: &[u8] = b"ERR_ILLEGAL_CALLER"; + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +struct Router { + /// The account id of the Aurora Engine instance that controls this router. + parent: LazyOption, + /// The version of the router contract that was last deployed + version: LazyOption, + /// A sequential id to keep track of how many scheduled promises this router has executed. + /// This allows multiple promises to be scheduled before any of them are executed. + nonce: LazyOption, + /// The storage for the scheduled promises. + scheduled_promises: LookupMap, +} + +#[near_bindgen] +impl Router { + #[init(ignore_state)] + pub fn initialize() -> Self { + // The first time this function is called there is no state and the parent is set to be + // the predecessor account id. In subsequent calls, only the original parent is allowed to + // call this function. The idea is that the Create, Deploy and Initialize actions are done in a single + // NEAR batch when a new router is deployed by the engine, so the caller will be the Aurora + // engine instance that the user's address belongs to. If we update this contract and deploy + // a new version of it, again the Deploy and Initialize actions will be done in a single batch + // by the engine. + let caller = env::predecessor_account_id(); + let mut parent = LazyOption::new(PARENT_PREFIX, None); + match parent.get() { + None => { + parent.set(&caller); + } + Some(parent) => { + if caller != parent { + env::panic(ERR_ILLEGAL_CALLER); + } + } + } + + let mut version = LazyOption::new(VERSION_PREFIX, None); + if version.get().unwrap_or_default() != CURRENT_VERSION { + // Future migrations would go here + + version.set(&CURRENT_VERSION); + } + + let nonce = LazyOption::new(NONCE_PREFIX, None); + let scheduled_promises = LookupMap::new(MAP_PREFIX); + Self { + parent, + version, + nonce, + scheduled_promises, + } + } + + /// This function can only be called by the parent account (i.e. Aurora engine) to ensure that + /// no one can create calls on behalf of the user this router contract is deployed for. + /// The engine only calls this function when the special precompile in the EVM for NEAR cross + /// contract calls is used by the address associated with the sub-account this router contract + /// is deployed at. + pub fn execute(&self, #[serializer(borsh)] promise: PromiseArgs) { + self.require_parent_caller(); + + let promise_id = Router::promise_create(promise); + env::promise_return(promise_id) + } + + /// Similar security considerations here as for `execute`. + pub fn schedule(&mut self, #[serializer(borsh)] promise: PromiseArgs) { + self.require_parent_caller(); + + let nonce = self.nonce.get().unwrap_or_default(); + self.scheduled_promises.insert(&nonce, &promise); + self.nonce.set(&(nonce + 1)); + } + + /// It is intentional that this function can be called by anyone (not just the parent). + /// There is no security risk to allowing this function to be open because it can only + /// act on promises that were created via `schedule`. + #[payable] + pub fn execute_scheduled(&mut self, nonce: U64) { + let promise = match self.scheduled_promises.remove(&nonce.0) { + Some(promise) => promise, + None => env::panic(b"ERR_PROMISE_NOT_FOUND"), + }; + + let promise_id = Router::promise_create(promise); + env::promise_return(promise_id) + } +} + +impl Router { + fn require_parent_caller(&self) { + let caller = env::predecessor_account_id(); + let parent = self + .parent + .get() + .unwrap_or_else(|| env::panic(b"ERR_CONTRACT_NOT_INITIALIZED")); + if caller != parent { + env::panic(ERR_ILLEGAL_CALLER) + } + } + + fn promise_create(promise: PromiseArgs) -> PromiseIndex { + match promise { + PromiseArgs::Create(call) => Self::base_promise_create(call), + PromiseArgs::Callback(cb) => Self::cb_promise_create(cb), + } + } + + fn cb_promise_create(promise: PromiseWithCallbackArgs) -> PromiseIndex { + let base = Self::base_promise_create(promise.base); + let promise = promise.callback; + env::promise_then( + base, + promise.target_account_id.to_string(), + promise.method.as_bytes(), + &promise.args, + promise.attached_balance.as_u128(), + promise.attached_gas.as_u64(), + ) + } + + fn base_promise_create(promise: PromiseCreateArgs) -> PromiseIndex { + env::promise_create( + promise.target_account_id.to_string(), + promise.method.as_bytes(), + &promise.args, + promise.attached_balance.as_u128(), + promise.attached_gas.as_u64(), + ) + } +} From 64d7304ae19c4c699cefa4018ca1c3549af6df27 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Fri, 29 Jul 2022 20:35:25 +0200 Subject: [PATCH 09/30] Add tests to router contract --- etc/xcc-router/Cargo.toml | 9 +- etc/xcc-router/src/lib.rs | 4 + etc/xcc-router/src/tests.rs | 310 ++++++++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 etc/xcc-router/src/tests.rs diff --git a/etc/xcc-router/Cargo.toml b/etc/xcc-router/Cargo.toml index aded56b05..5a12f71e2 100644 --- a/etc/xcc-router/Cargo.toml +++ b/etc/xcc-router/Cargo.toml @@ -5,7 +5,14 @@ authors = ["Aurora "] edition = "2021" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" [dependencies] aurora-engine-types = { path = "../../engine-types", default-features = false } diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index 0af535fd8..45380236c 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -4,6 +4,10 @@ use near_sdk::collections::{LazyOption, LookupMap}; use near_sdk::json_types::U64; use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault, PromiseIndex}; +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod tests; + const VERSION_PREFIX: &[u8] = &[0x00]; const PARENT_PREFIX: &[u8] = &[0x01]; const NONCE_PREFIX: &[u8] = &[0x02]; diff --git a/etc/xcc-router/src/tests.rs b/etc/xcc-router/src/tests.rs new file mode 100644 index 000000000..f0b75685d --- /dev/null +++ b/etc/xcc-router/src/tests.rs @@ -0,0 +1,310 @@ +use super::*; +use aurora_engine_types::parameters::{PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs}; +use aurora_engine_types::types::{NearGas, Yocto}; +use near_sdk::test_utils::test_env::{alice, bob, carol}; +use near_sdk::test_utils::{self, VMContextBuilder}; +use near_sdk::{serde_json, testing_env, MockedBlockchain}; + +#[test] +fn test_initialize() { + let (parent, contract) = create_contract(); + + assert_eq!(contract.parent.get().unwrap(), parent); +} + +/// `initialize` should be able to be called multiple times without resetting the state. +#[test] +fn test_reinitialize() { + let (_parent, mut contract) = create_contract(); + + let nonce = 8; + contract.nonce.set(&nonce); + drop(contract); + + let contract = Router::initialize(); + assert_eq!(contract.nonce.get().unwrap(), nonce); +} + +// If an account other than the parent calls `initialize` it panics. +#[test] +#[should_panic] +fn test_reinitialize_wrong_caller() { + let (parent, contract) = create_contract(); + + assert_eq!(contract.parent.get().unwrap(), parent); + drop(contract); + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(bob().try_into().unwrap()) + .build()); + let _contract = Router::initialize(); +} + +#[test] +#[should_panic] +fn test_execute_wrong_caller() { + let (_parent, contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(100_000_000_000_000), + }; + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(bob().try_into().unwrap()) + .build()); + contract.execute(PromiseArgs::Create(promise)); +} + +#[test] +fn test_execute() { + let (_parent, contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(100_000_000_000_000), + }; + + contract.execute(PromiseArgs::Create(promise.clone())); + + let mut receipts = Receipt::get_created_receipts(); + assert_eq!(receipts.len(), 1); + let receipt = receipts.pop().unwrap(); + assert_eq!(receipt.receiver_id(), promise.target_account_id.as_ref()); + + validate_function_call_action(&receipt.actions(), promise); +} + +#[test] +fn test_execute_callback() { + let (_parent, contract) = create_contract(); + + let promise = PromiseWithCallbackArgs { + base: PromiseCreateArgs { + target_account_id: bob().parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(5678), + attached_gas: NearGas::new(100_000_000_000_000), + }, + callback: PromiseCreateArgs { + target_account_id: carol().parse().unwrap(), + method: "another_method".into(), + args: b"goodbye_world".to_vec(), + attached_balance: Yocto::new(567), + attached_gas: NearGas::new(10_000_000_000_000), + }, + }; + + contract.execute(PromiseArgs::Callback(promise.clone())); + + let receipts = Receipt::get_created_receipts(); + assert_eq!(receipts.len(), 2); + let base = &receipts[0]; + let callback = &receipts[1]; + + validate_function_call_action(&base.actions(), promise.base); + validate_function_call_action(&callback.actions(), promise.callback); +} + +#[test] +#[should_panic] +fn test_schedule_wrong_caller() { + let (_parent, mut contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(100_000_000_000_000), + }; + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(bob().try_into().unwrap()) + .build()); + contract.schedule(PromiseArgs::Create(promise)); +} + +#[test] +fn test_schedule_and_execute() { + let (_parent, mut contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(100_000_000_000_000), + }; + + contract.schedule(PromiseArgs::Create(promise.clone())); + + // no promise actually create yet + let receipts = Receipt::get_created_receipts(); + assert!(receipts.is_empty()); + + // promise stored and nonce incremented instead + assert_eq!(contract.nonce.get().unwrap(), 1); + let stored_promise = match contract.scheduled_promises.get(&0) { + Some(PromiseArgs::Create(promise)) => promise, + _ => unreachable!(), + }; + assert_eq!(stored_promise, promise); + + // promise executed after calling `execute_scheduled` + // anyone can call this function + testing_env!(VMContextBuilder::new() + .predecessor_account_id(bob().try_into().unwrap()) + .build()); + contract.execute_scheduled(0.into()); + + assert_eq!(contract.nonce.get().unwrap(), 1); + assert!(!contract.scheduled_promises.contains_key(&0)); + + let mut receipts = Receipt::get_created_receipts(); + assert_eq!(receipts.len(), 1); + let receipt = receipts.pop().unwrap(); + assert_eq!(receipt.receiver_id(), promise.target_account_id.as_ref()); + validate_function_call_action(&receipt.actions(), promise); +} + +fn validate_function_call_action(actions: &[Action], promise: PromiseCreateArgs) { + assert_eq!(actions.len(), 1); + let action = &actions[0]; + assert_eq!( + action.function_call().method_name(), + promise.method.as_str() + ); + assert_eq!(action.function_call().args(), promise.args.as_slice()); + assert_eq!(action.function_call().gas(), promise.attached_gas); + assert_eq!(action.function_call().deposit(), promise.attached_balance); +} + +fn create_contract() -> (String, Router) { + let parent = alice(); + testing_env!(VMContextBuilder::new() + .current_account_id(format!("some_address.{}", parent).try_into().unwrap()) + .predecessor_account_id(parent.as_str().try_into().unwrap()) + .build()); + let contract = Router::initialize(); + + (parent, contract) +} + +/// Cannot use the `Receipt` type from `test_utils::get_created_receipts` for introspection +/// because all the fields are private. As a work-around we serialize the object to json. +#[derive(Debug)] +struct Receipt { + underlying: serde_json::Value, +} + +impl Receipt { + fn get_created_receipts() -> Vec { + let receipts = test_utils::get_created_receipts(); + receipts + .iter() + .map(|v| serde_json::to_string(v).unwrap()) + .map(|v| Self { + underlying: serde_json::from_str(&v).unwrap(), + }) + .collect() + } + + fn receiver_id(&self) -> &str { + self.underlying + .as_object() + .unwrap() + .get("receiver_id") + .unwrap() + .as_str() + .unwrap() + } + + fn actions(&self) -> Vec { + self.underlying + .as_object() + .unwrap() + .get("actions") + .unwrap() + .as_array() + .unwrap() + .iter() + .map(|v| Action { underlying: v }) + .collect() + } +} + +struct Action<'a> { + underlying: &'a serde_json::Value, +} + +impl<'a> Action<'a> { + fn function_call(&self) -> FunctionCall { + FunctionCall { + underlying: self + .underlying + .as_object() + .unwrap() + .get("FunctionCall") + .unwrap(), + } + } +} + +struct FunctionCall<'a> { + underlying: &'a serde_json::Value, +} + +impl<'a> FunctionCall<'a> { + fn method_name(&self) -> &str { + self.underlying + .as_object() + .unwrap() + .get("method_name") + .unwrap() + .as_str() + .unwrap() + } + + fn args(&self) -> &[u8] { + self.underlying + .as_object() + .unwrap() + .get("args") + .unwrap() + .as_str() + .unwrap() + .as_bytes() + } + + fn gas(&self) -> NearGas { + NearGas::new( + self.underlying + .as_object() + .unwrap() + .get("gas") + .unwrap() + .as_u64() + .unwrap(), + ) + } + + fn deposit(&self) -> Yocto { + Yocto::new( + self.underlying + .as_object() + .unwrap() + .get("deposit") + .unwrap() + .as_u64() + .unwrap() as u128, + ) + } +} From 3908520bf6edc934ef2fab7c8a7918cb4cf9117d Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Mon, 1 Aug 2022 20:36:35 +0200 Subject: [PATCH 10/30] Integration tests for cross contract calls --- engine-precompiles/src/xcc.rs | 7 +- engine-tests/src/tests/erc20_connector.rs | 6 +- engine-tests/src/tests/mod.rs | 3 +- engine-tests/src/tests/xcc.rs | 232 ++++++++++++++++++++++ engine/src/xcc.rs | 13 +- etc/xcc-router/src/lib.rs | 2 + 6 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 engine-tests/src/tests/xcc.rs diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index dc99fd24c..38099d83a 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -23,15 +23,14 @@ const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE"; const ROUTER_EXEC_NAME: &str = "execute"; const ROUTER_SCHEDULE_NAME: &str = "schedule"; -mod costs { +pub mod costs { use crate::prelude::types::{EthGas, NearGas}; // TODO(#483): Determine the correct amount of gas pub(super) const CROSS_CONTRACT_CALL: EthGas = EthGas::new(0); - // TODO: Determine the correct amount of gas - pub(super) const ROUTER_EXEC: NearGas = NearGas::new(0); - pub(super) const ROUTER_SCHEDULE: NearGas = NearGas::new(0); + pub const ROUTER_EXEC: NearGas = NearGas::new(7_000_000_000_000); + pub const ROUTER_SCHEDULE: NearGas = NearGas::new(5_000_000_000_000); } pub struct CrossContractCall { diff --git a/engine-tests/src/tests/erc20_connector.rs b/engine-tests/src/tests/erc20_connector.rs index be83cac26..5689876c8 100644 --- a/engine-tests/src/tests/erc20_connector.rs +++ b/engine-tests/src/tests/erc20_connector.rs @@ -391,7 +391,7 @@ fn test_transfer_erc20_token() { // Simulation tests for exit to NEAR precompile. // Note: `AuroraRunner` is not suitable for these tests because // it does not execute promises; but `near-sdk-sim` does. -mod sim_tests { +pub mod sim_tests { use crate::prelude::{Wei, WeiU256, U256}; use crate::test_utils; use crate::test_utils::erc20::{ERC20Constructor, ERC20}; @@ -879,7 +879,7 @@ mod sim_tests { ERC20(crate::test_utils::solidity::DeployedContract { abi, address }) } - fn nep_141_balance_of( + pub fn nep_141_balance_of( account_id: &str, nep_141: &near_sdk_sim::UserAccount, aurora: &AuroraAccount, @@ -902,7 +902,7 @@ mod sim_tests { /// Deploys the standard FT implementation: /// https://github.com/near/near-sdk-rs/blob/master/examples/fungible-token/ft/src/lib.rs - fn deploy_nep_141( + pub fn deploy_nep_141( nep_141_account_id: &str, token_owner: &str, amount: u128, diff --git a/engine-tests/src/tests/mod.rs b/engine-tests/src/tests/mod.rs index e6fd6b7b4..720c72019 100644 --- a/engine-tests/src/tests/mod.rs +++ b/engine-tests/src/tests/mod.rs @@ -5,7 +5,7 @@ mod ecrecover; mod eip1559; mod erc20; mod erc20_connector; -mod eth_connector; +pub mod eth_connector; mod ghsa_3p69_m8gg_fwmf; #[cfg(feature = "meta-call")] mod meta_parsing; @@ -21,3 +21,4 @@ mod standalone; mod standard_precompiles; mod state_migration; pub(crate) mod uniswap; +mod xcc; diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs new file mode 100644 index 000000000..08be41af1 --- /dev/null +++ b/engine-tests/src/tests/xcc.rs @@ -0,0 +1,232 @@ +use crate::test_utils::{self, AuroraRunner}; +use crate::tests::erc20_connector::sim_tests; +use crate::tests::state_migration::deploy_evm; +use aurora_engine_precompiles::xcc::{costs, cross_contract_call}; +use aurora_engine_transactions::legacy::TransactionLegacy; +use aurora_engine_types::parameters::{CrossContractCallArgs, PromiseArgs, PromiseCreateArgs}; +use aurora_engine_types::types::{NearGas, Wei, Yocto}; +use borsh::BorshSerialize; +use near_primitives::transaction::Action; +use near_primitives_core::contract::ContractCode; +use std::fs; +use std::path::Path; + +#[test] +fn test_xcc_precompile() { + let aurora = deploy_evm(); + let xcc_wasm_bytes = contract_bytes(); + aurora + .user + .call( + aurora.contract.account_id(), + "factory_update", + &xcc_wasm_bytes, + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + + let mut signer = test_utils::Signer::random(); + let signer_address = test_utils::address_from_secret_key(&signer.secret_key); + let router_account = format!( + "{}.{}", + hex::encode(signer_address.as_bytes()), + aurora.contract.account_id.as_str() + ); + + // 1. Deploy NEP-141 token. + let ft_owner = aurora.user.create_user( + "ft_owner.root".parse().unwrap(), + near_sdk_sim::STORAGE_AMOUNT, + ); + let nep_141_supply = 500; + let nep_141_token = sim_tests::deploy_nep_141( + "test_token.root", + ft_owner.account_id.as_ref(), + nep_141_supply, + &aurora, + ); + + // 2. Register EVM router contract + let args = serde_json::json!({ + "account_id": router_account, + }) + .to_string(); + aurora + .user + .call( + nep_141_token.account_id(), + "storage_deposit", + args.as_bytes(), + near_sdk_sim::DEFAULT_GAS, + near_sdk_sim::STORAGE_AMOUNT, + ) + .assert_success(); + + // 3. Give router some tokens + let transfer_amount: u128 = 199; + let args = serde_json::json!({ + "receiver_id": router_account, + "amount": format!("{}", transfer_amount), + }) + .to_string(); + ft_owner + .call( + nep_141_token.account_id(), + "ft_transfer", + args.as_bytes(), + near_sdk_sim::DEFAULT_GAS, + 1, + ) + .assert_success(); + assert_eq!( + sim_tests::nep_141_balance_of(ft_owner.account_id.as_str(), &nep_141_token, &aurora), + nep_141_supply - transfer_amount + ); + + // 4. Use xcc precompile to send those tokens back + let args = serde_json::json!({ + "receiver_id": ft_owner.account_id.as_str(), + "amount": format!("{}", transfer_amount), + }) + .to_string(); + let promise = PromiseCreateArgs { + target_account_id: nep_141_token.account_id.as_str().parse().unwrap(), + method: "ft_transfer".into(), + args: args.into_bytes(), + attached_balance: Yocto::new(1), + attached_gas: NearGas::new(100_000_000_000_000), + }; + let transaction = TransactionLegacy { + nonce: signer.use_nonce().into(), + gas_price: 0u64.into(), + gas_limit: u64::MAX.into(), + to: Some(cross_contract_call::ADDRESS), + value: Wei::zero(), + data: CrossContractCallArgs::Eager(PromiseArgs::Create(promise)) + .try_to_vec() + .unwrap(), + }; + let signed_transaction = test_utils::sign_transaction( + transaction, + Some(AuroraRunner::default().chain_id), + &signer.secret_key, + ); + aurora + .user + .call( + aurora.contract.account_id(), + "submit", + &rlp::encode(&signed_transaction), + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + + let rt = aurora.user.borrow_runtime(); + for id in rt.last_outcomes.iter() { + println!("{:?}\n\n", rt.outcome(id).unwrap()); + } + drop(rt); + + assert_eq!( + sim_tests::nep_141_balance_of(ft_owner.account_id.as_str(), &nep_141_token, &aurora), + nep_141_supply + ); +} + +#[test] +fn test_xcc_schedule_gas() { + let mut router = deploy_router(); + + let promise = PromiseCreateArgs { + target_account_id: "some_account.near".parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(100_000_000_000_000), + }; + + let (maybe_outcome, maybe_error) = router.call( + "schedule", + "aurora", + PromiseArgs::Create(promise.clone()).try_to_vec().unwrap(), + ); + assert!(maybe_error.is_none()); + let outcome = maybe_outcome.unwrap(); + assert!( + outcome.burnt_gas < costs::ROUTER_SCHEDULE.as_u64(), + "{:?} not less than {:?}", + outcome.burnt_gas, + costs::ROUTER_SCHEDULE + ); + assert_eq!(outcome.logs.len(), 1); + assert_eq!(outcome.logs[0], "Promise scheduled at nonce 0"); +} + +#[test] +fn test_xcc_exec_gas() { + let mut router = deploy_router(); + + let promise = PromiseCreateArgs { + target_account_id: "some_account.near".parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(100_000_000_000_000), + }; + + let (maybe_outcome, maybe_error) = router.call( + "execute", + "aurora", + PromiseArgs::Create(promise.clone()).try_to_vec().unwrap(), + ); + assert!(maybe_error.is_none()); + let outcome = maybe_outcome.unwrap(); + + assert!( + outcome.burnt_gas < costs::ROUTER_EXEC.as_u64(), + "{:?} not less than {:?}", + outcome.burnt_gas, + costs::ROUTER_EXEC + ); + assert_eq!(outcome.action_receipts.len(), 1); + assert_eq!( + outcome.action_receipts[0].0.as_str(), + promise.target_account_id.as_ref() + ); + let receipt = &outcome.action_receipts[0].1; + assert_eq!(receipt.actions.len(), 1); + let action = &receipt.actions[0]; + match action { + Action::FunctionCall(function_call) => { + assert_eq!(function_call.method_name, promise.method); + assert_eq!(function_call.args, promise.args); + assert_eq!(function_call.deposit, promise.attached_balance.as_u128()); + assert_eq!(function_call.gas, promise.attached_gas.as_u64()); + } + other => panic!("Unexpected action {:?}", other), + }; +} + +fn deploy_router() -> AuroraRunner { + let mut router = AuroraRunner::default(); + router.code = ContractCode::new(contract_bytes(), None); + + router.context.current_account_id = "some_address.aurora".parse().unwrap(); + router.context.predecessor_account_id = "aurora".parse().unwrap(); + + let (maybe_outcome, maybe_error) = router.call("initialize", "aurora", Vec::new()); + assert!(maybe_error.is_none()); + let outcome = maybe_outcome.unwrap(); + assert!(outcome.used_gas < aurora_engine::xcc::INITIALIZE_GAS.as_u64()); + + router +} + +fn contract_bytes() -> Vec { + let base_path = Path::new("../etc").join("xcc-router"); + let output_path = base_path.join("target/wasm32-unknown-unknown/release/xcc_router.wasm"); + test_utils::rust::compile(base_path); + fs::read(output_path).unwrap() +} diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index 47e5f657c..ba46c6bb1 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -4,7 +4,7 @@ use aurora_engine_sdk::promise::PromiseHandler; use aurora_engine_types::account_id::AccountId; use aurora_engine_types::parameters::{PromiseAction, PromiseBatchAction, PromiseCreateArgs}; use aurora_engine_types::storage::{self, KeyPrefix}; -use aurora_engine_types::types::{Address, NearGas, ZERO_YOCTO}; +use aurora_engine_types::types::{Address, NearGas, Yocto, ZERO_YOCTO}; use aurora_engine_types::Vec; use borsh::{BorshDeserialize, BorshSerialize}; @@ -13,9 +13,10 @@ pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; pub const ERR_INVALID_ACCOUNT: &str = "ERR_INVALID_XCC_ACCOUNT"; pub const VERSION_KEY: &[u8] = b"version"; pub const CODE_KEY: &[u8] = b"router_code"; -// TODO: estimate gas -pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(0); -pub const INITIALIZE_GAS: NearGas = NearGas::new(0); +pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(5_000_000_000_000); +pub const INITIALIZE_GAS: NearGas = NearGas::new(5_000_000_000_000); +/// Amount of NEAR needed to cover storage for a router contract. +pub const STORAGE_AMOUNT: Yocto = Yocto::new(5_000_000_000_000_000_000_000_000); /// Type wrapper for version of router contracts. #[derive( @@ -66,6 +67,9 @@ pub fn handle_precomile_promise( let mut promise_actions = Vec::with_capacity(4); if create_needed { promise_actions.push(PromiseAction::CreateAccount); + promise_actions.push(PromiseAction::Transfer { + amount: STORAGE_AMOUNT, + }); } promise_actions.push(PromiseAction::DeployConotract { code: get_router_code(io).0, @@ -73,7 +77,6 @@ pub fn handle_precomile_promise( // After a deploy we call the contract's initialize function promise_actions.push(PromiseAction::FunctionCall { name: "initialize".into(), - // TODO: initialize args? args: Vec::new(), attached_yocto: ZERO_YOCTO, gas: INITIALIZE_GAS, diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index 45380236c..08b415a75 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -91,6 +91,8 @@ impl Router { let nonce = self.nonce.get().unwrap_or_default(); self.scheduled_promises.insert(&nonce, &promise); self.nonce.set(&(nonce + 1)); + + env::log(format!("Promise scheduled at nonce {}", nonce).as_bytes()); } /// It is intentional that this function can be called by anyone (not just the parent). From 0c06a199a7c49b028ae7668ce16d12eb4b5d97cd Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Mon, 1 Aug 2022 20:53:07 +0200 Subject: [PATCH 11/30] Ingration tests for both eager promise execution and scheduled promies --- engine-tests/src/tests/xcc.rs | 40 +++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs index 08be41af1..542b7c81a 100644 --- a/engine-tests/src/tests/xcc.rs +++ b/engine-tests/src/tests/xcc.rs @@ -12,7 +12,16 @@ use std::fs; use std::path::Path; #[test] -fn test_xcc_precompile() { +fn test_xcc_precompile_eager() { + test_xcc_precompile_common(false) +} + +#[test] +fn test_xcc_precompile_scheduled() { + test_xcc_precompile_common(true) +} + +fn test_xcc_precompile_common(is_scheduled: bool) { let aurora = deploy_evm(); let xcc_wasm_bytes = contract_bytes(); aurora @@ -97,15 +106,18 @@ fn test_xcc_precompile() { attached_balance: Yocto::new(1), attached_gas: NearGas::new(100_000_000_000_000), }; + let xcc_args = if is_scheduled { + CrossContractCallArgs::Delayed(PromiseArgs::Create(promise)) + } else { + CrossContractCallArgs::Eager(PromiseArgs::Create(promise)) + }; let transaction = TransactionLegacy { nonce: signer.use_nonce().into(), gas_price: 0u64.into(), gas_limit: u64::MAX.into(), to: Some(cross_contract_call::ADDRESS), value: Wei::zero(), - data: CrossContractCallArgs::Eager(PromiseArgs::Create(promise)) - .try_to_vec() - .unwrap(), + data: xcc_args.try_to_vec().unwrap(), }; let signed_transaction = test_utils::sign_transaction( transaction, @@ -129,6 +141,26 @@ fn test_xcc_precompile() { } drop(rt); + if is_scheduled { + // The promise was only scheduled, not executed immediately. So the FT balance has not changed yet. + assert_eq!( + sim_tests::nep_141_balance_of(ft_owner.account_id.as_str(), &nep_141_token, &aurora), + nep_141_supply - transfer_amount + ); + + // Now we execute the scheduled promise + aurora + .user + .call( + router_account.parse().unwrap(), + "execute_scheduled", + b"{\"nonce\": \"0\"}", + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + } + assert_eq!( sim_tests::nep_141_balance_of(ft_owner.account_id.as_str(), &nep_141_token, &aurora), nep_141_supply From 1dcef9142dd7dae436c1d94f796806292f3bd02d Mon Sep 17 00:00:00 2001 From: Marcelo Fornet Date: Tue, 2 Aug 2022 20:46:25 +0200 Subject: [PATCH 12/30] Feat(xcc-router): Use version 4.0.0 of near-sdk-rs (#562) --- Cargo.lock | 116 +-- engine-precompiles/Cargo.toml | 2 +- engine-sdk/Cargo.toml | 2 +- engine-standalone-storage/Cargo.toml | 2 +- engine-tests/Cargo.toml | 6 +- engine-types/Cargo.toml | 2 +- engine/Cargo.toml | 2 +- etc/tests/self-contained-5bEgfRQ/Cargo.toml | 2 +- etc/tests/state-migration-test/Cargo.toml | 2 +- etc/xcc-router/Cargo.lock | 757 +++++++++++++++++--- etc/xcc-router/Cargo.toml | 2 +- etc/xcc-router/src/lib.rs | 47 +- etc/xcc-router/src/tests.rs | 181 ++--- 13 files changed, 760 insertions(+), 363 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51d22d5c9..66bd6dce6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - [[package]] name = "ahash" version = "0.7.6" @@ -113,7 +107,7 @@ dependencies = [ "aurora-engine-transactions", "aurora-engine-types", "base64 0.13.0", - "borsh 0.8.2", + "borsh", "byte-slice-cast", "ethabi", "evm", @@ -132,7 +126,7 @@ version = "1.0.0" dependencies = [ "aurora-engine-sdk", "aurora-engine-types", - "borsh 0.8.2", + "borsh", "ethabi", "evm", "hex", @@ -152,7 +146,7 @@ name = "aurora-engine-sdk" version = "1.0.0" dependencies = [ "aurora-engine-types", - "borsh 0.8.2", + "borsh", "sha2 0.10.2", "sha3 0.10.2", ] @@ -167,7 +161,7 @@ dependencies = [ "aurora-engine-transactions", "aurora-engine-types", "base64 0.13.0", - "borsh 0.8.2", + "borsh", "bstr", "byte-slice-cast", "criterion", @@ -213,7 +207,7 @@ dependencies = [ name = "aurora-engine-types" version = "1.0.0" dependencies = [ - "borsh 0.8.2", + "borsh", "hex", "primitive-types 0.11.1", "rand 0.7.3", @@ -374,63 +368,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" -[[package]] -name = "borsh" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" -dependencies = [ - "borsh-derive 0.8.2", - "hashbrown 0.9.1", -] - [[package]] name = "borsh" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ - "borsh-derive 0.9.3", + "borsh-derive", "hashbrown 0.11.2", ] -[[package]] -name = "borsh-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" -dependencies = [ - "borsh-derive-internal 0.8.2", - "borsh-schema-derive-internal 0.8.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn", -] - [[package]] name = "borsh-derive" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ - "borsh-derive-internal 0.9.3", - "borsh-schema-derive-internal 0.9.3", + "borsh-derive-internal", + "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", "proc-macro2", "syn", ] -[[package]] -name = "borsh-derive-internal" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "borsh-derive-internal" version = "0.9.3" @@ -442,17 +402,6 @@ dependencies = [ "syn", ] -[[package]] -name = "borsh-schema-derive-internal" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "borsh-schema-derive-internal" version = "0.9.3" @@ -1114,7 +1063,7 @@ dependencies = [ "aurora-engine-transactions", "aurora-engine-types", "base64 0.13.0", - "borsh 0.8.2", + "borsh", "evm-core", "postgres", "rocksdb", @@ -1552,22 +1501,13 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.7", -] - [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] @@ -1576,7 +1516,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] @@ -2135,7 +2075,7 @@ name = "near-account-id" version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ - "borsh 0.9.3", + "borsh", "serde", ] @@ -2172,7 +2112,7 @@ source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0db dependencies = [ "arrayref", "blake2", - "borsh 0.9.3", + "borsh", "bs58", "c2-chacha", "curve25519-dalek", @@ -2225,7 +2165,7 @@ name = "near-pool" version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ - "borsh 0.9.3", + "borsh", "near-crypto", "near-metrics", "near-primitives", @@ -2238,7 +2178,7 @@ name = "near-primitives" version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ - "borsh 0.9.3", + "borsh", "byteorder", "bytesize", "chrono", @@ -2267,7 +2207,7 @@ version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ "base64 0.11.0", - "borsh 0.9.3", + "borsh", "bs58", "derive_more", "near-account-id", @@ -2300,10 +2240,10 @@ dependencies = [ [[package]] name = "near-sdk" version = "3.1.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07#ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=7a3fa3fbff84b712050370d840297df38c925d2d#7a3fa3fbff84b712050370d840297df38c925d2d" dependencies = [ "base64 0.13.0", - "borsh 0.8.2", + "borsh", "bs58", "near-primitives-core", "near-sdk-macros", @@ -2316,7 +2256,7 @@ dependencies = [ [[package]] name = "near-sdk-core" version = "3.1.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07#ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=7a3fa3fbff84b712050370d840297df38c925d2d#7a3fa3fbff84b712050370d840297df38c925d2d" dependencies = [ "Inflector", "proc-macro2", @@ -2327,7 +2267,7 @@ dependencies = [ [[package]] name = "near-sdk-macros" version = "3.1.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07#ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=7a3fa3fbff84b712050370d840297df38c925d2d#7a3fa3fbff84b712050370d840297df38c925d2d" dependencies = [ "near-sdk-core", "proc-macro2", @@ -2338,7 +2278,7 @@ dependencies = [ [[package]] name = "near-sdk-sim" version = "3.2.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07#ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=7a3fa3fbff84b712050370d840297df38c925d2d#7a3fa3fbff84b712050370d840297df38c925d2d" dependencies = [ "chrono", "funty 1.1.0", @@ -2363,7 +2303,7 @@ name = "near-store" version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ - "borsh 0.9.3", + "borsh", "byteorder", "bytesize", "derive_more", @@ -2391,7 +2331,7 @@ name = "near-vm-errors" version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ - "borsh 0.9.3", + "borsh", "near-account-id", "near-rpc-error-macro", "serde", @@ -2403,7 +2343,7 @@ version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ "base64 0.13.0", - "borsh 0.9.3", + "borsh", "bs58", "byteorder", "near-account-id", @@ -2424,7 +2364,7 @@ version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ "anyhow", - "borsh 0.9.3", + "borsh", "loupe", "memoffset", "near-cache", @@ -2469,7 +2409,7 @@ name = "node-runtime" version = "0.0.0" source = "git+https://github.com/birchmd/nearcore.git?rev=980bc48dc02878fea1e0dbc5812ae7de49f12dda#980bc48dc02878fea1e0dbc5812ae7de49f12dda" dependencies = [ - "borsh 0.9.3", + "borsh", "byteorder", "hex", "near-chain-configs", @@ -4505,7 +4445,7 @@ checksum = "5a3fac37da3c625e98708c5dd92d3f642aaf700fd077168d3d0fff277ec6a165" dependencies = [ "bincode", "blake3", - "borsh 0.9.3", + "borsh", "cc", "digest 0.8.1", "errno", @@ -4548,7 +4488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6edd0ba6c0bcf9b279186d4dbe81649dda3e5ef38f586865943de4dcd653f8" dependencies = [ "bincode", - "borsh 0.9.3", + "borsh", "byteorder", "dynasm", "dynasmrt", diff --git a/engine-precompiles/Cargo.toml b/engine-precompiles/Cargo.toml index 39ebf03a4..ccdc9232c 100644 --- a/engine-precompiles/Cargo.toml +++ b/engine-precompiles/Cargo.toml @@ -15,7 +15,7 @@ autobenches = false [dependencies] aurora-engine-types = { path = "../engine-types", default-features = false } aurora-engine-sdk = { path = "../engine-sdk", default-features = false } -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } bn = { version = "0.5.11", package = "zeropool-bn", default-features = false } evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.36.0-aurora", default-features = false } libsecp256k1 = { version = "0.7.0", default-features = false, features = ["static-context", "hmac"] } diff --git a/engine-sdk/Cargo.toml b/engine-sdk/Cargo.toml index dc3d576be..c6a127a03 100644 --- a/engine-sdk/Cargo.toml +++ b/engine-sdk/Cargo.toml @@ -14,7 +14,7 @@ autobenches = false [dependencies] aurora-engine-types = { path = "../engine-types", default-features = false } -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } sha3 = { version = "0.10.2", default-features = false } sha2 = { version = "0.10.2", default-features = false } diff --git a/engine-standalone-storage/Cargo.toml b/engine-standalone-storage/Cargo.toml index f6f70109c..26aee6edc 100644 --- a/engine-standalone-storage/Cargo.toml +++ b/engine-standalone-storage/Cargo.toml @@ -18,7 +18,7 @@ aurora-engine = { path = "../engine", default-features = false, features = ["std aurora-engine-types = { path = "../engine-types", default-features = false, features = ["std"] } aurora-engine-sdk = { path = "../engine-sdk", default-features = false, features = ["std"] } aurora-engine-transactions = { path = "../engine-transactions", default-features = false, features = ["std"] } -borsh = { version = "0.8.2" } +borsh = { version = "0.9" } evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.36.0-aurora", default-features = false } rocksdb = { version = "0.18.0", default-features = false } postgres = "0.19.2" diff --git a/engine-tests/Cargo.toml b/engine-tests/Cargo.toml index 3d9863ffe..b74330172 100644 --- a/engine-tests/Cargo.toml +++ b/engine-tests/Cargo.toml @@ -22,7 +22,7 @@ aurora-engine-precompiles = { path = "../engine-precompiles", default-features = aurora-engine-transactions = { path = "../engine-transactions", default-features = false, features = ["std"] } engine-standalone-storage = { path = "../engine-standalone-storage" } engine-standalone-tracing = { path = "../engine-standalone-tracing" } -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } sha3 = { version = "0.10.2", default-features = false } evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.36.0-aurora", default-features = false, features = ["std", "tracing"] } evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.36.0-aurora", default-features = false, features = ["std", "tracing"] } @@ -35,8 +35,8 @@ ethabi = "17.1" serde = { version = "1", features = ["derive"] } serde_json = "1" hex = { version = "0.4.3", default-features = false } -near-sdk = { git = "https://github.com/aurora-is-near/near-sdk-rs.git", rev = "ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07" } -near-sdk-sim = { git = "https://github.com/aurora-is-near/near-sdk-rs.git", rev = "ba2eddbfbf4484ac3e44b4c8119bbac4907d6e07" } +near-sdk = { git = "https://github.com/aurora-is-near/near-sdk-rs.git", rev = "7a3fa3fbff84b712050370d840297df38c925d2d" } +near-sdk-sim = { git = "https://github.com/aurora-is-near/near-sdk-rs.git", rev = "7a3fa3fbff84b712050370d840297df38c925d2d" } near-crypto = { git = "https://github.com/birchmd/nearcore.git", rev = "980bc48dc02878fea1e0dbc5812ae7de49f12dda" } near-vm-runner = { git = "https://github.com/birchmd/nearcore.git", rev = "980bc48dc02878fea1e0dbc5812ae7de49f12dda", default-features = false, features = [ "wasmer2_vm", "protocol_feature_alt_bn128" ] } near-vm-logic = { git = "https://github.com/birchmd/nearcore.git", rev = "980bc48dc02878fea1e0dbc5812ae7de49f12dda", default-features = false, features = [ "protocol_feature_alt_bn128" ] } diff --git a/engine-types/Cargo.toml b/engine-types/Cargo.toml index 430e9b735..4494c0580 100644 --- a/engine-types/Cargo.toml +++ b/engine-types/Cargo.toml @@ -13,7 +13,7 @@ publish = false autobenches = false [dependencies] -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } primitive-types = { version = "0.11", default-features = false, features = ["rlp"] } serde = { version = "1", features = ["derive"], optional = true } diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 4f2594d82..f9fa20cd3 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -21,7 +21,7 @@ aurora-engine-sdk = { path = "../engine-sdk", default-features = false } aurora-engine-precompiles = { path = "../engine-precompiles", default-features = false } aurora-engine-transactions = { path = "../engine-transactions", default-features = false } base64 = { version = "0.13.0", default-features = false, features = ["alloc"] } -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } byte-slice-cast = { version = "1.0", default-features = false } ethabi = { version = "17.1", default-features = false } evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.36.0-aurora", default-features = false } diff --git a/etc/tests/self-contained-5bEgfRQ/Cargo.toml b/etc/tests/self-contained-5bEgfRQ/Cargo.toml index d8ee6b226..8a69714e0 100644 --- a/etc/tests/self-contained-5bEgfRQ/Cargo.toml +++ b/etc/tests/self-contained-5bEgfRQ/Cargo.toml @@ -37,7 +37,7 @@ codegen-units = 1 rpath = false [dependencies] -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } aurora-engine = { path = "../../../engine", default-features = false } aurora-engine-sdk = { path = "../../../engine-sdk", default-features = false, features = ["contract"] } aurora-engine-types = { path = "../../../engine-types", default-features = false } diff --git a/etc/tests/state-migration-test/Cargo.toml b/etc/tests/state-migration-test/Cargo.toml index 6af872762..bf645e4b0 100644 --- a/etc/tests/state-migration-test/Cargo.toml +++ b/etc/tests/state-migration-test/Cargo.toml @@ -37,7 +37,7 @@ codegen-units = 1 rpath = false [dependencies] -borsh = { version = "0.8.2", default-features = false } +borsh = { version = "0.9.3", default-features = false } aurora-engine = { path = "../../../engine", default-features = false } aurora-engine-sdk = { path = "../../../engine-sdk", default-features = false, features = ["contract"] } aurora-engine-types = { path = "../../../engine-types", default-features = false } diff --git a/etc/xcc-router/Cargo.lock b/etc/xcc-router/Cargo.lock index fde33fb83..27ad8dfa5 100644 --- a/etc/xcc-router/Cargo.lock +++ b/etc/xcc-router/Cargo.lock @@ -10,17 +10,13 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "ahash" -version = "0.4.7" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "memchr", + "getrandom 0.2.7", + "once_cell", + "version_check", ] [[package]] @@ -29,6 +25,24 @@ version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "aurora-engine-types" version = "1.0.0" @@ -46,12 +60,41 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +dependencies = [ + "crypto-mac", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -62,6 +105,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.2.1" @@ -70,32 +122,32 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "borsh" -version = "0.8.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ "borsh-derive", - "hashbrown 0.9.1", + "hashbrown", ] [[package]] name = "borsh-derive" -version = "0.8.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "syn", ] [[package]] name = "borsh-derive-internal" -version = "0.8.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ "proc-macro2", "quote", @@ -104,9 +156,9 @@ dependencies = [ [[package]] name = "borsh-schema-derive-internal" -version = "0.8.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ "proc-macro2", "quote", @@ -119,6 +171,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "byte-slice-cast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" + [[package]] name = "byteorder" version = "1.4.3" @@ -131,6 +189,28 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + +[[package]] +name = "c2-chacha" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27dae93fe7b1e0424dc57179ac396908c26b035a87234809f5c4dfd1b47dc80" +dependencies = [ + "cipher", + "ppv-lite86", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfg-if" version = "0.1.10" @@ -143,6 +223,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -164,6 +267,39 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -186,6 +322,45 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + +[[package]] +name = "easy-ext" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31" + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ethabi" version = "14.1.0" @@ -228,10 +403,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", + "rand 0.8.5", "rustc-hex", "static_assertions", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "generic-array" version = "0.14.5" @@ -242,20 +424,42 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ "ahash", ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "heck" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hex" @@ -263,6 +467,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + [[package]] name = "impl-rlp" version = "0.3.0" @@ -273,13 +486,14 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.1" +name = "impl-trait-for-tuples" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -294,24 +508,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - [[package]] name = "memory_units" version = "0.4.0" @@ -319,71 +521,124 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" [[package]] -name = "near-primitives-core" -version = "0.4.0" +name = "near-account-id" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de83d74a9241be8cc4eb3055216966b58bf8c463e8e285c0dc553925acdd19fa" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "near-crypto" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2b3fb5acf3a494aed4e848446ef2d6ebb47dbe91c681105d4d1786c2ee63e52" +checksum = "b8ecf0b8b31aa7f4e60f629f72213a2617ca4a5f45cd1ae9ed2cf7cecfebdbb7" dependencies = [ - "base64", + "arrayref", + "blake2", "borsh", "bs58", + "c2-chacha", + "curve25519-dalek", + "derive_more", + "ed25519-dalek", + "libc", + "near-account-id", + "once_cell", + "parity-secp256k1", + "primitive-types", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "serde_json", + "subtle", + "thiserror", +] + +[[package]] +name = "near-primitives" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ba19282e79a4485a77736b679d276b09870bbf8042a18e0f0ae36347489c5" +dependencies = [ + "borsh", + "byteorder", + "bytesize", + "chrono", "derive_more", + "easy-ext", "hex", - "lazy_static", + "near-crypto", + "near-primitives-core", + "near-rpc-error-macro", + "near-vm-errors", "num-rational", + "once_cell", + "primitive-types", + "rand 0.7.3", + "reed-solomon-erasure", "serde", "serde_json", - "sha2", + "smart-default", + "strum", + "thiserror", ] [[package]] -name = "near-rpc-error-core" -version = "0.1.0" +name = "near-primitives-core" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa8dbf8437a28ac40fcb85859ab0d0b8385013935b000c7a51ae79631dd74d9" +checksum = "bb561feb392bb8c4f540256073446e6689af087bf6356e8dddcf75fc279f201f" dependencies = [ - "proc-macro2", - "quote", + "base64 0.11.0", + "borsh", + "bs58", + "derive_more", + "near-account-id", + "num-rational", "serde", - "serde_json", - "syn", + "sha2 0.10.2", + "strum", ] [[package]] -name = "near-rpc-error-macro" -version = "0.1.0" +name = "near-rpc-error-core" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6111d713e90c7c551dee937f4a06cb9ea2672243455a4454cc7566387ba2d9" +checksum = "77fdd7ea8d8f786878651c37691515d5053f827ae60894aa40c16882b78f77c9" dependencies = [ - "near-rpc-error-core", - "proc-macro2", "quote", "serde", - "serde_json", "syn", ] [[package]] -name = "near-runtime-utils" -version = "4.0.0-pre.1" +name = "near-rpc-error-macro" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48d80c4ca1d4cf99bc16490e1e3d49826c150dfc4410ac498918e45c7d98e07" +checksum = "e521842b6ae864dfe5391afbbe2df9e9d8427c26e9333b2e0b65cd42094f7607" dependencies = [ - "lazy_static", - "regex", + "near-rpc-error-core", + "serde", + "syn", ] [[package]] name = "near-sdk" -version = "3.1.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7383e242d3e07bf0951e8589d6eebd7f18bb1c1fc5fbec3fad796041a6aebd1" +checksum = "bda34e06e28fb9a09ac54efbdc49f0c9308780fc932aaa81c49c493fde974045" dependencies = [ - "base64", + "base64 0.13.0", "borsh", "bs58", + "near-crypto", + "near-primitives", "near-primitives-core", "near-sdk-macros", + "near-sys", "near-vm-logic", "serde", "serde_json", @@ -391,10 +646,10 @@ dependencies = [ ] [[package]] -name = "near-sdk-core" -version = "3.1.0" +name = "near-sdk-macros" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284a78d9eb8eda58330462fa0023a6d7014c941df1f0387095e7dfd1dc0f2bce" +checksum = "72064fcc15a623a0d40a6c199ea5cbdc30a83cae4816889d46f218acf31bfba8" dependencies = [ "Inflector", "proc-macro2", @@ -403,44 +658,41 @@ dependencies = [ ] [[package]] -name = "near-sdk-macros" -version = "3.1.0" +name = "near-sys" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2037337438f97d1ce5f7c896cf229dc56dacd5c01142d1ef95a7d778cde6ce7d" -dependencies = [ - "near-sdk-core", - "proc-macro2", - "quote", - "syn", -] +checksum = "e307313276eaeced2ca95740b5639e1f3125b7c97f0a1151809d105f1aa8c6d3" [[package]] name = "near-vm-errors" -version = "4.0.0-pre.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281d8730ed8cb0e3e69fb689acee6b93cdb43824cd69a8ffd7e1bfcbd1177d7" +checksum = "0e02faf2bc1f6ef82b965cfe44389808fb5594f7aca4b596766117f4ce74df20" dependencies = [ "borsh", - "hex", + "near-account-id", "near-rpc-error-macro", "serde", ] [[package]] name = "near-vm-logic" -version = "4.0.0-pre.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11cb28a2d07f37680efdaf860f4c9802828c44fc50c08009e7884de75d982c5" +checksum = "f024d90451cd3c24d7a0a5cabf3636b192a60eb8e3ff0456f6c18b91152c346d" dependencies = [ - "base64", + "base64 0.13.0", "borsh", "bs58", "byteorder", + "near-account-id", + "near-crypto", + "near-primitives", "near-primitives-core", - "near-runtime-utils", "near-vm-errors", + "ripemd", "serde", - "sha2", + "sha2 0.9.9", "sha3", ] @@ -487,12 +739,62 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec 0.7.2", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate 1.2.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parity-secp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fca4f82fccae37e8bbdaeb949a4a218a1bbc485d11598f193d2a908042e5fc1" +dependencies = [ + "arrayvec 0.5.2", + "cc", + "cfg-if 0.1.10", + "rand 0.7.3", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "primitive-types" version = "0.10.1" @@ -500,6 +802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" dependencies = [ "fixed-hash", + "impl-codec", "impl-rlp", "uint", ] @@ -513,6 +816,17 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d50bfb8c23f23915855a00d98b5a35ef2e0b871bb52937bacadb798fbb66c8" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + [[package]] name = "proc-macro2" version = "1.0.42" @@ -532,21 +846,99 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.6.0" +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.7", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "reed-solomon-erasure" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "a415a013dd7c5d4221382329a5a3482566da675737494935cbbbcdec04662f9d" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "smallvec", ] [[package]] -name = "regex-syntax" -version = "0.6.27" +name = "ripemd" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "1facec54cb5e0dc08553501fa740091086d0259ad0067e0d4103448e4cb22ed3" +dependencies = [ + "digest 0.10.3", +] [[package]] name = "rlp" @@ -573,6 +965,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" + [[package]] name = "ryu" version = "1.0.10" @@ -611,7 +1009,6 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ - "indexmap", "itoa", "ryu", "serde", @@ -623,31 +1020,93 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha3" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "keccak", "opaque-debug", ] +[[package]] +name = "signature" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.57" @@ -659,6 +1118,55 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -713,6 +1221,24 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wee_alloc" version = "0.4.5" @@ -747,6 +1273,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "xcc_router" version = "1.0.0" @@ -754,3 +1286,24 @@ dependencies = [ "aurora-engine-types", "near-sdk", ] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/etc/xcc-router/Cargo.toml b/etc/xcc-router/Cargo.toml index 5a12f71e2..ba56690af 100644 --- a/etc/xcc-router/Cargo.toml +++ b/etc/xcc-router/Cargo.toml @@ -16,4 +16,4 @@ panic = "abort" [dependencies] aurora-engine-types = { path = "../../engine-types", default-features = false } -near-sdk = "3.1.0" +near-sdk = "4.0.0" diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index 08b415a75..528293fc4 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -2,24 +2,28 @@ use aurora_engine_types::parameters::{PromiseArgs, PromiseCreateArgs, PromiseWit use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; use near_sdk::json_types::U64; +use near_sdk::BorshStorageKey; use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault, PromiseIndex}; #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod tests; -const VERSION_PREFIX: &[u8] = &[0x00]; -const PARENT_PREFIX: &[u8] = &[0x01]; -const NONCE_PREFIX: &[u8] = &[0x02]; -const MAP_PREFIX: &[u8] = &[0x03]; +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey { + Version, + Parent, + Nonce, + Map, +} const CURRENT_VERSION: u32 = 0; -const ERR_ILLEGAL_CALLER: &[u8] = b"ERR_ILLEGAL_CALLER"; +const ERR_ILLEGAL_CALLER: &str = "ERR_ILLEGAL_CALLER"; #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] -struct Router { +pub struct Router { /// The account id of the Aurora Engine instance that controls this router. parent: LazyOption, /// The version of the router contract that was last deployed @@ -43,27 +47,27 @@ impl Router { // a new version of it, again the Deploy and Initialize actions will be done in a single batch // by the engine. let caller = env::predecessor_account_id(); - let mut parent = LazyOption::new(PARENT_PREFIX, None); + let mut parent = LazyOption::new(StorageKey::Parent, None); match parent.get() { None => { parent.set(&caller); } Some(parent) => { if caller != parent { - env::panic(ERR_ILLEGAL_CALLER); + env::panic_str(ERR_ILLEGAL_CALLER); } } } - let mut version = LazyOption::new(VERSION_PREFIX, None); + let mut version = LazyOption::new(StorageKey::Version, None); if version.get().unwrap_or_default() != CURRENT_VERSION { // Future migrations would go here version.set(&CURRENT_VERSION); } - let nonce = LazyOption::new(NONCE_PREFIX, None); - let scheduled_promises = LookupMap::new(MAP_PREFIX); + let nonce = LazyOption::new(StorageKey::Nonce, None); + let scheduled_promises = LookupMap::new(StorageKey::Map); Self { parent, version, @@ -92,7 +96,7 @@ impl Router { self.scheduled_promises.insert(&nonce, &promise); self.nonce.set(&(nonce + 1)); - env::log(format!("Promise scheduled at nonce {}", nonce).as_bytes()); + near_sdk::log!("Promise scheduled at nonce {}", nonce); } /// It is intentional that this function can be called by anyone (not just the parent). @@ -102,7 +106,7 @@ impl Router { pub fn execute_scheduled(&mut self, nonce: U64) { let promise = match self.scheduled_promises.remove(&nonce.0) { Some(promise) => promise, - None => env::panic(b"ERR_PROMISE_NOT_FOUND"), + None => env::panic_str("ERR_PROMISE_NOT_FOUND"), }; let promise_id = Router::promise_create(promise); @@ -116,9 +120,9 @@ impl Router { let parent = self .parent .get() - .unwrap_or_else(|| env::panic(b"ERR_CONTRACT_NOT_INITIALIZED")); + .unwrap_or_else(|| env::panic_str("ERR_CONTRACT_NOT_INITIALIZED")); if caller != parent { - env::panic(ERR_ILLEGAL_CALLER) + env::panic_str(ERR_ILLEGAL_CALLER) } } @@ -132,23 +136,24 @@ impl Router { fn cb_promise_create(promise: PromiseWithCallbackArgs) -> PromiseIndex { let base = Self::base_promise_create(promise.base); let promise = promise.callback; + env::promise_then( base, - promise.target_account_id.to_string(), - promise.method.as_bytes(), + near_sdk::AccountId::new_unchecked(promise.target_account_id.to_string()), + promise.method.as_str(), &promise.args, promise.attached_balance.as_u128(), - promise.attached_gas.as_u64(), + promise.attached_gas.as_u64().into(), ) } fn base_promise_create(promise: PromiseCreateArgs) -> PromiseIndex { env::promise_create( - promise.target_account_id.to_string(), - promise.method.as_bytes(), + near_sdk::AccountId::new_unchecked(promise.target_account_id.to_string()), + promise.method.as_str(), &promise.args, promise.attached_balance.as_u128(), - promise.attached_gas.as_u64(), + promise.attached_gas.as_u64().into(), ) } } diff --git a/etc/xcc-router/src/tests.rs b/etc/xcc-router/src/tests.rs index f0b75685d..586c6a661 100644 --- a/etc/xcc-router/src/tests.rs +++ b/etc/xcc-router/src/tests.rs @@ -1,9 +1,10 @@ -use super::*; +use super::Router; use aurora_engine_types::parameters::{PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs}; use aurora_engine_types::types::{NearGas, Yocto}; +use near_sdk::mock::VmAction; use near_sdk::test_utils::test_env::{alice, bob, carol}; use near_sdk::test_utils::{self, VMContextBuilder}; -use near_sdk::{serde_json, testing_env, MockedBlockchain}; +use near_sdk::testing_env; #[test] fn test_initialize() { @@ -35,7 +36,7 @@ fn test_reinitialize_wrong_caller() { drop(contract); testing_env!(VMContextBuilder::new() - .predecessor_account_id(bob().try_into().unwrap()) + .predecessor_account_id(bob()) .build()); let _contract = Router::initialize(); } @@ -46,7 +47,7 @@ fn test_execute_wrong_caller() { let (_parent, contract) = create_contract(); let promise = PromiseCreateArgs { - target_account_id: bob().parse().unwrap(), + target_account_id: bob().as_str().parse().unwrap(), method: "some_method".into(), args: b"hello_world".to_vec(), attached_balance: Yocto::new(56), @@ -54,7 +55,7 @@ fn test_execute_wrong_caller() { }; testing_env!(VMContextBuilder::new() - .predecessor_account_id(bob().try_into().unwrap()) + .predecessor_account_id(bob()) .build()); contract.execute(PromiseArgs::Create(promise)); } @@ -64,7 +65,7 @@ fn test_execute() { let (_parent, contract) = create_contract(); let promise = PromiseCreateArgs { - target_account_id: bob().parse().unwrap(), + target_account_id: bob().as_str().parse().unwrap(), method: "some_method".into(), args: b"hello_world".to_vec(), attached_balance: Yocto::new(56), @@ -73,12 +74,15 @@ fn test_execute() { contract.execute(PromiseArgs::Create(promise.clone())); - let mut receipts = Receipt::get_created_receipts(); + let mut receipts = test_utils::get_created_receipts(); assert_eq!(receipts.len(), 1); let receipt = receipts.pop().unwrap(); - assert_eq!(receipt.receiver_id(), promise.target_account_id.as_ref()); + assert_eq!( + receipt.receiver_id.as_str(), + promise.target_account_id.as_ref() + ); - validate_function_call_action(&receipt.actions(), promise); + validate_function_call_action(&receipt.actions, promise); } #[test] @@ -87,14 +91,14 @@ fn test_execute_callback() { let promise = PromiseWithCallbackArgs { base: PromiseCreateArgs { - target_account_id: bob().parse().unwrap(), + target_account_id: bob().as_str().parse().unwrap(), method: "some_method".into(), args: b"hello_world".to_vec(), attached_balance: Yocto::new(5678), attached_gas: NearGas::new(100_000_000_000_000), }, callback: PromiseCreateArgs { - target_account_id: carol().parse().unwrap(), + target_account_id: carol().as_str().parse().unwrap(), method: "another_method".into(), args: b"goodbye_world".to_vec(), attached_balance: Yocto::new(567), @@ -104,13 +108,13 @@ fn test_execute_callback() { contract.execute(PromiseArgs::Callback(promise.clone())); - let receipts = Receipt::get_created_receipts(); + let receipts = test_utils::get_created_receipts(); assert_eq!(receipts.len(), 2); let base = &receipts[0]; let callback = &receipts[1]; - validate_function_call_action(&base.actions(), promise.base); - validate_function_call_action(&callback.actions(), promise.callback); + validate_function_call_action(&base.actions, promise.base); + validate_function_call_action(&callback.actions, promise.callback); } #[test] @@ -119,7 +123,7 @@ fn test_schedule_wrong_caller() { let (_parent, mut contract) = create_contract(); let promise = PromiseCreateArgs { - target_account_id: bob().parse().unwrap(), + target_account_id: bob().as_str().parse().unwrap(), method: "some_method".into(), args: b"hello_world".to_vec(), attached_balance: Yocto::new(56), @@ -127,7 +131,7 @@ fn test_schedule_wrong_caller() { }; testing_env!(VMContextBuilder::new() - .predecessor_account_id(bob().try_into().unwrap()) + .predecessor_account_id(bob()) .build()); contract.schedule(PromiseArgs::Create(promise)); } @@ -137,7 +141,7 @@ fn test_schedule_and_execute() { let (_parent, mut contract) = create_contract(); let promise = PromiseCreateArgs { - target_account_id: bob().parse().unwrap(), + target_account_id: bob().as_str().parse().unwrap(), method: "some_method".into(), args: b"hello_world".to_vec(), attached_balance: Yocto::new(56), @@ -147,7 +151,7 @@ fn test_schedule_and_execute() { contract.schedule(PromiseArgs::Create(promise.clone())); // no promise actually create yet - let receipts = Receipt::get_created_receipts(); + let receipts = test_utils::get_created_receipts(); assert!(receipts.is_empty()); // promise stored and nonce incremented instead @@ -161,150 +165,45 @@ fn test_schedule_and_execute() { // promise executed after calling `execute_scheduled` // anyone can call this function testing_env!(VMContextBuilder::new() - .predecessor_account_id(bob().try_into().unwrap()) + .predecessor_account_id(bob()) .build()); contract.execute_scheduled(0.into()); assert_eq!(contract.nonce.get().unwrap(), 1); assert!(!contract.scheduled_promises.contains_key(&0)); - let mut receipts = Receipt::get_created_receipts(); + let mut receipts = test_utils::get_created_receipts(); assert_eq!(receipts.len(), 1); let receipt = receipts.pop().unwrap(); - assert_eq!(receipt.receiver_id(), promise.target_account_id.as_ref()); - validate_function_call_action(&receipt.actions(), promise); + assert_eq!( + receipt.receiver_id.as_str(), + promise.target_account_id.as_ref() + ); + validate_function_call_action(&receipt.actions, promise); } -fn validate_function_call_action(actions: &[Action], promise: PromiseCreateArgs) { +fn validate_function_call_action(actions: &[VmAction], promise: PromiseCreateArgs) { assert_eq!(actions.len(), 1); let action = &actions[0]; + assert_eq!( - action.function_call().method_name(), - promise.method.as_str() + *action, + VmAction::FunctionCall { + function_name: promise.method, + args: promise.args, + gas: promise.attached_gas.as_u64().into(), + deposit: promise.attached_balance.as_u128() + } ); - assert_eq!(action.function_call().args(), promise.args.as_slice()); - assert_eq!(action.function_call().gas(), promise.attached_gas); - assert_eq!(action.function_call().deposit(), promise.attached_balance); } -fn create_contract() -> (String, Router) { +fn create_contract() -> (near_sdk::AccountId, Router) { let parent = alice(); testing_env!(VMContextBuilder::new() .current_account_id(format!("some_address.{}", parent).try_into().unwrap()) - .predecessor_account_id(parent.as_str().try_into().unwrap()) + .predecessor_account_id(parent.clone()) .build()); let contract = Router::initialize(); (parent, contract) } - -/// Cannot use the `Receipt` type from `test_utils::get_created_receipts` for introspection -/// because all the fields are private. As a work-around we serialize the object to json. -#[derive(Debug)] -struct Receipt { - underlying: serde_json::Value, -} - -impl Receipt { - fn get_created_receipts() -> Vec { - let receipts = test_utils::get_created_receipts(); - receipts - .iter() - .map(|v| serde_json::to_string(v).unwrap()) - .map(|v| Self { - underlying: serde_json::from_str(&v).unwrap(), - }) - .collect() - } - - fn receiver_id(&self) -> &str { - self.underlying - .as_object() - .unwrap() - .get("receiver_id") - .unwrap() - .as_str() - .unwrap() - } - - fn actions(&self) -> Vec { - self.underlying - .as_object() - .unwrap() - .get("actions") - .unwrap() - .as_array() - .unwrap() - .iter() - .map(|v| Action { underlying: v }) - .collect() - } -} - -struct Action<'a> { - underlying: &'a serde_json::Value, -} - -impl<'a> Action<'a> { - fn function_call(&self) -> FunctionCall { - FunctionCall { - underlying: self - .underlying - .as_object() - .unwrap() - .get("FunctionCall") - .unwrap(), - } - } -} - -struct FunctionCall<'a> { - underlying: &'a serde_json::Value, -} - -impl<'a> FunctionCall<'a> { - fn method_name(&self) -> &str { - self.underlying - .as_object() - .unwrap() - .get("method_name") - .unwrap() - .as_str() - .unwrap() - } - - fn args(&self) -> &[u8] { - self.underlying - .as_object() - .unwrap() - .get("args") - .unwrap() - .as_str() - .unwrap() - .as_bytes() - } - - fn gas(&self) -> NearGas { - NearGas::new( - self.underlying - .as_object() - .unwrap() - .get("gas") - .unwrap() - .as_u64() - .unwrap(), - ) - } - - fn deposit(&self) -> Yocto { - Yocto::new( - self.underlying - .as_object() - .unwrap() - .get("deposit") - .unwrap() - .as_u64() - .unwrap() as u128, - ) - } -} From f24def83f66e6ebcfdb3c34c62a0029faf28e96f Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 13:30:13 +0200 Subject: [PATCH 13/30] Fix typo --- engine/src/engine.rs | 2 +- engine/src/xcc.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 278524d9c..59980c69f 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -1393,7 +1393,7 @@ where } } else if log.address == cross_contract_call::ADDRESS.raw() { if let Ok(promise) = PromiseCreateArgs::try_from_slice(&log.data) { - crate::xcc::handle_precomile_promise(io, handler, promise, current_account_id); + crate::xcc::handle_precompile_promise(io, handler, promise, current_account_id); } // do not pass on these "internal logs" to caller None diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index ba46c6bb1..384dd4c98 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -40,7 +40,7 @@ pub struct AddressVersionUpdateArgs { pub version: CodeVersion, } -pub fn handle_precomile_promise( +pub fn handle_precompile_promise( io: &I, handler: &mut P, promise: PromiseCreateArgs, From 13d2533bea36503a376c6921338f38318234f02b Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 13:42:31 +0200 Subject: [PATCH 14/30] Verify target_account is of the form {address}.{aurora} --- engine/src/xcc.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index 384dd4c98..f1098f97f 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -51,6 +51,17 @@ pub fn handle_precompile_promise( { let target_account: &str = promise.target_account_id.as_ref(); let sender = Address::decode(&target_account[0..40]).expect(ERR_INVALID_ACCOUNT); + + // Confirm target_account is of the form `{address}.{aurora}` + // Address prefix parsed above, so only need to check `.{aurora}` + assert_eq!(&target_account[40..41], ".", "{}", ERR_INVALID_ACCOUNT); + assert_eq!( + &target_account[41..], + current_account_id.as_ref(), + "{}", + ERR_INVALID_ACCOUNT + ); + let latest_code_version = get_latest_code_version(io); let sender_code_version = get_code_version_of_address(io, &sender); let deploy_needed = match sender_code_version { From 94a5241199e396defa802fb0342e1ec86788d9d2 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 13:48:47 +0200 Subject: [PATCH 15/30] Set current xcc-router version to 1 --- etc/xcc-router/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index 528293fc4..07beef4b5 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -17,7 +17,7 @@ enum StorageKey { Map, } -const CURRENT_VERSION: u32 = 0; +const CURRENT_VERSION: u32 = 1; const ERR_ILLEGAL_CALLER: &str = "ERR_ILLEGAL_CALLER"; From 1234b9124d28cf49ef4f80c094db80fcee5caa38 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 13:58:41 +0200 Subject: [PATCH 16/30] Set xcc router storage amount to 4 NEAR --- engine/src/xcc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index f1098f97f..eeed435e1 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -16,7 +16,7 @@ pub const CODE_KEY: &[u8] = b"router_code"; pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(5_000_000_000_000); pub const INITIALIZE_GAS: NearGas = NearGas::new(5_000_000_000_000); /// Amount of NEAR needed to cover storage for a router contract. -pub const STORAGE_AMOUNT: Yocto = Yocto::new(5_000_000_000_000_000_000_000_000); +pub const STORAGE_AMOUNT: Yocto = Yocto::new(4_000_000_000_000_000_000_000_000); /// Type wrapper for version of router contracts. #[derive( From 60cc71b303d96ec49438be6fe55350ac2b00485d Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 14:08:29 +0200 Subject: [PATCH 17/30] Check xcc precompile does not create promises with attached NEAR --- engine/src/xcc.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index eeed435e1..e1b6eb20a 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -11,6 +11,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub const ERR_NO_ROUTER_CODE: &str = "ERR_MISSING_XCC_BYTECODE"; pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; pub const ERR_INVALID_ACCOUNT: &str = "ERR_INVALID_XCC_ACCOUNT"; +pub const ERR_ATTACHED_NEAR: &str = "ERR_ATTACHED_XCC_NEAR"; pub const VERSION_KEY: &[u8] = b"version"; pub const CODE_KEY: &[u8] = b"router_code"; pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(5_000_000_000_000); @@ -61,6 +62,13 @@ pub fn handle_precompile_promise( "{}", ERR_INVALID_ACCOUNT ); + // Confirm there is 0 NEAR attached to the promise + // (the precompile should not drain the engine's balance). + assert_eq!( + promise.attached_balance, ZERO_YOCTO, + "{}", + ERR_ATTACHED_NEAR + ); let latest_code_version = get_latest_code_version(io); let sender_code_version = get_code_version_of_address(io, &sender); From 6271db3e530a6efdbd65ab3dab9ca1d7cb90b70b Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 14:28:34 +0200 Subject: [PATCH 18/30] Standalone implementations for factory_update and factory_update_address_version --- engine-standalone-storage/src/sync/mod.rs | 13 ++++++++++++- engine-standalone-storage/src/sync/types.rs | 15 +++++++++++++++ engine/src/lib.rs | 2 +- engine/src/xcc.rs | 17 ++++++++++++++--- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 89c6d2f48..467064cde 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -1,4 +1,4 @@ -use aurora_engine::{connector, engine, parameters::SubmitResult}; +use aurora_engine::{connector, engine, parameters::SubmitResult, xcc}; use aurora_engine_sdk::env::{self, Env, DEFAULT_PREPAID_GAS}; use aurora_engine_types::{ account_id::AccountId, @@ -369,6 +369,17 @@ fn non_submit_execute<'db>( None } + TransactionKind::FactoryUpdate(bytecode) => { + let router_bytecode = xcc::RouterCode::borrowed(bytecode); + xcc::update_router_code(&mut io, &router_bytecode); + + None + } + TransactionKind::FactoryUpdateAddressVersion(args) => { + xcc::set_code_version_of_address(&mut io, &args.address, args.version); + + None + } TransactionKind::Unknown => None, // Not handled in this function; is handled by the general `execute_transaction` function TransactionKind::Submit(_) => unreachable!(), diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index 395ebfbeb..f220753e0 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -1,4 +1,5 @@ use aurora_engine::parameters; +use aurora_engine::xcc::AddressVersionUpdateArgs; use aurora_engine_transactions::EthTransactionKind; use aurora_engine_types::account_id::AccountId; use aurora_engine_types::{types, H256}; @@ -108,6 +109,10 @@ pub enum TransactionKind { NewConnector(parameters::InitCallArgs), /// Initialize Engine NewEngine(parameters::NewCallArgs), + /// Update xcc-router bytecode + FactoryUpdate(Vec), + /// Update the version of a deployed xcc-router contract + FactoryUpdateAddressVersion(AddressVersionUpdateArgs), /// Sentinel kind for cases where a NEAR receipt caused a /// change in Aurora state, but we failed to parse the Action. Unknown, @@ -234,6 +239,8 @@ enum BorshableTransactionKind<'a> { SetConnectorData(Cow<'a, parameters::SetContractDataCallArgs>), NewConnector(Cow<'a, parameters::InitCallArgs>), NewEngine(Cow<'a, parameters::NewCallArgs>), + FactoryUpdate(Cow<'a, Vec>), + FactoryUpdateAddressVersion(Cow<'a, AddressVersionUpdateArgs>), Unknown, } @@ -265,6 +272,10 @@ impl<'a> From<&'a TransactionKind> for BorshableTransactionKind<'a> { TransactionKind::SetConnectorData(x) => Self::SetConnectorData(Cow::Borrowed(x)), TransactionKind::NewConnector(x) => Self::NewConnector(Cow::Borrowed(x)), TransactionKind::NewEngine(x) => Self::NewEngine(Cow::Borrowed(x)), + TransactionKind::FactoryUpdate(x) => Self::FactoryUpdate(Cow::Borrowed(x)), + TransactionKind::FactoryUpdateAddressVersion(x) => { + Self::FactoryUpdateAddressVersion(Cow::Borrowed(x)) + } TransactionKind::Unknown => Self::Unknown, } } @@ -309,6 +320,10 @@ impl<'a> TryFrom> for TransactionKind { } BorshableTransactionKind::NewConnector(x) => Ok(Self::NewConnector(x.into_owned())), BorshableTransactionKind::NewEngine(x) => Ok(Self::NewEngine(x.into_owned())), + BorshableTransactionKind::FactoryUpdate(x) => Ok(Self::FactoryUpdate(x.into_owned())), + BorshableTransactionKind::FactoryUpdateAddressVersion(x) => { + Ok(Self::FactoryUpdateAddressVersion(x.into_owned())) + } BorshableTransactionKind::Unknown => Ok(Self::Unknown), } } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 1dca7b59d..d2c130732 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -292,7 +292,7 @@ mod contract { let state = engine::get_state(&io).sdk_unwrap(); require_owner_only(&state, &io.predecessor_account_id()); let bytes = io.read_input().to_vec(); - let router_bytecode = crate::xcc::RouterCode(bytes); + let router_bytecode = crate::xcc::RouterCode::new(bytes); crate::xcc::update_router_code(&mut io, &router_bytecode); } diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index e1b6eb20a..76936f6e1 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -5,6 +5,7 @@ use aurora_engine_types::account_id::AccountId; use aurora_engine_types::parameters::{PromiseAction, PromiseBatchAction, PromiseCreateArgs}; use aurora_engine_types::storage::{self, KeyPrefix}; use aurora_engine_types::types::{Address, NearGas, Yocto, ZERO_YOCTO}; +use aurora_engine_types::Cow; use aurora_engine_types::Vec; use borsh::{BorshDeserialize, BorshSerialize}; @@ -33,7 +34,17 @@ impl CodeVersion { /// Type wrapper for router bytecode. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct RouterCode(pub Vec); +pub struct RouterCode<'a>(pub Cow<'a, [u8]>); + +impl<'a> RouterCode<'a> { + pub fn new(bytes: Vec) -> Self { + Self(Cow::Owned(bytes)) + } + + pub fn borrowed(bytes: &'a [u8]) -> Self { + Self(Cow::Borrowed(bytes)) + } +} #[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] pub struct AddressVersionUpdateArgs { @@ -91,7 +102,7 @@ pub fn handle_precompile_promise( }); } promise_actions.push(PromiseAction::DeployConotract { - code: get_router_code(io).0, + code: get_router_code(io).0.into_owned(), }); // After a deploy we call the contract's initialize function promise_actions.push(PromiseAction::FunctionCall { @@ -136,7 +147,7 @@ pub fn handle_precompile_promise( pub fn get_router_code(io: &I) -> RouterCode { let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, CODE_KEY); let bytes = io.read_storage(&key).expect(ERR_NO_ROUTER_CODE).to_vec(); - RouterCode(bytes) + RouterCode::new(bytes) } /// Set new router bytecode, and update increment the version by 1. From 18698944002f6ad0a37e79ddd856800df64c5813 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 14:29:42 +0200 Subject: [PATCH 19/30] Remove XCC.md --- XCC.md | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 XCC.md diff --git a/XCC.md b/XCC.md deleted file mode 100644 index 160a95f56..000000000 --- a/XCC.md +++ /dev/null @@ -1,27 +0,0 @@ -# Cross contract call - -## Engine interface - -- Propose: Any user can propose a new contract to be used as proxy contract. It is stored on-chain. -- Accept: Admin should accept some proposed contract. The current version is bumped! - -## Engine storage - -Maintain a list of all deployed contracts, with the version of the current deployed bytecode. Version is an integer bigger than 0. - -## Host function - -Whenever an updated is needed, a new proxy bytecode is proposed and accepted. Going forward before calling the proxy, -the new version will be deployed and initialized. - -Method init must be agnostic to the current state of the contract, it is possible updating a contract -skipping an arbitrary number of versions. - -### Promise anatomy - -When a call to xcc host function is made, a bundle is created using the concatenation of the following promises: - -- create-account (only if account hasn't been created yet) -- deploy-contract (only if current deployed contract doesn't match current version) -- init-call (only if deploy contrac must be called) -- xcc-access (alwasy) \ No newline at end of file From 4e76bda6e427d23caea35630f8153f8b00cf5885 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 14:31:18 +0200 Subject: [PATCH 20/30] Remove TODO, borsh encoding is fine --- engine-precompiles/src/xcc.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 38099d83a..68803c378 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -85,7 +85,6 @@ impl Precompile for CrossContractCall { let sender = context.caller; let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref()); - // TODO: Is it ok to use Borsh to read the input? It might not be very friendly to construct the input in Solidity... let args = CrossContractCallArgs::try_from_slice(input) .map_err(|_| ExitError::Other(Cow::from(ERR_INVALID_INPUT)))?; let promise = match args { From 0a86302b59db82a0a2f4240c442544f09bde284e Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 14:35:09 +0200 Subject: [PATCH 21/30] Add PromiseArgs::total_gas function --- engine-precompiles/src/xcc.rs | 7 ++----- engine-types/src/parameters.rs | 9 +++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 68803c378..225e87234 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -8,7 +8,7 @@ use aurora_engine_sdk::io::IO; use aurora_engine_types::{ account_id::AccountId, format, - parameters::{CrossContractCallArgs, PromiseArgs, PromiseCreateArgs}, + parameters::{CrossContractCallArgs, PromiseCreateArgs}, types::{balance::ZERO_YOCTO, EthGas}, vec, Cow, Vec, H160, }; @@ -89,10 +89,7 @@ impl Precompile for CrossContractCall { .map_err(|_| ExitError::Other(Cow::from(ERR_INVALID_INPUT)))?; let promise = match args { CrossContractCallArgs::Eager(call) => { - let call_gas = match &call { - PromiseArgs::Create(call) => call.attached_gas, - PromiseArgs::Callback(cb) => cb.base.attached_gas + cb.callback.attached_gas, - }; + let call_gas = call.total_gas(); PromiseCreateArgs { target_account_id, method: ROUTER_EXEC_NAME.into(), diff --git a/engine-types/src/parameters.rs b/engine-types/src/parameters.rs index 8a07bde19..629e612c1 100644 --- a/engine-types/src/parameters.rs +++ b/engine-types/src/parameters.rs @@ -10,6 +10,15 @@ pub enum PromiseArgs { Callback(PromiseWithCallbackArgs), } +impl PromiseArgs { + pub fn total_gas(&self) -> NearGas { + match self { + Self::Create(call) => call.attached_gas, + Self::Callback(cb) => cb.base.attached_gas + cb.callback.attached_gas, + } + } +} + #[must_use] #[derive(Debug, BorshSerialize, BorshDeserialize, Clone, PartialEq, Eq)] pub struct PromiseCreateArgs { From 21df808fd7fd0ed76eac3ddbc712a1667abb9f38 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 15:46:27 +0200 Subject: [PATCH 22/30] Reduce xcc-router storage cost to 2 NEAR --- engine-tests/src/test_utils/rust.rs | 1 + engine/src/xcc.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/engine-tests/src/test_utils/rust.rs b/engine-tests/src/test_utils/rust.rs index d02569be0..ce10882df 100644 --- a/engine-tests/src/test_utils/rust.rs +++ b/engine-tests/src/test_utils/rust.rs @@ -4,6 +4,7 @@ use std::process::Command; pub fn compile>(source_path: P) { let output = Command::new("cargo") .current_dir(source_path) + .env("RUSTFLAGS", "-C link-arg=-s") .args(&["build", "--target", "wasm32-unknown-unknown", "--release"]) .output() .unwrap(); diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index 76936f6e1..fc7130f7e 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -18,7 +18,7 @@ pub const CODE_KEY: &[u8] = b"router_code"; pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(5_000_000_000_000); pub const INITIALIZE_GAS: NearGas = NearGas::new(5_000_000_000_000); /// Amount of NEAR needed to cover storage for a router contract. -pub const STORAGE_AMOUNT: Yocto = Yocto::new(4_000_000_000_000_000_000_000_000); +pub const STORAGE_AMOUNT: Yocto = Yocto::new(2_000_000_000_000_000_000_000_000); /// Type wrapper for version of router contracts. #[derive( From c28a33c86bd0bb15900ec07acd7d7dc0c44ab344 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 3 Aug 2022 21:48:04 +0200 Subject: [PATCH 23/30] Move error messages to mod consts --- engine-precompiles/src/xcc.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 225e87234..1afcf9aa7 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -16,13 +16,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use evm::backend::Log; use evm_core::ExitError; -const ERR_INVALID_INPUT: &str = "ERR_INVALID_XCC_INPUT"; -const ERR_SERIALIZE: &str = "ERR_XCC_CALL_SERIALIZE"; -const ERR_STATIC: &str = "ERR_INVALID_IN_STATIC"; -const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE"; -const ROUTER_EXEC_NAME: &str = "execute"; -const ROUTER_SCHEDULE_NAME: &str = "schedule"; - pub mod costs { use crate::prelude::types::{EthGas, NearGas}; @@ -33,6 +26,15 @@ pub mod costs { pub const ROUTER_SCHEDULE: NearGas = NearGas::new(5_000_000_000_000); } +mod consts { + pub(super) const ERR_INVALID_INPUT: &str = "ERR_INVALID_XCC_INPUT"; + pub(super) const ERR_SERIALIZE: &str = "ERR_XCC_CALL_SERIALIZE"; + pub(super) const ERR_STATIC: &str = "ERR_INVALID_IN_STATIC"; + pub(super) const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE"; + pub(super) const ROUTER_EXEC_NAME: &str = "execute"; + pub(super) const ROUTER_SCHEDULE_NAME: &str = "schedule"; +} + pub struct CrossContractCall { io: I, engine_account_id: AccountId, @@ -78,34 +80,34 @@ impl Precompile for CrossContractCall { // It's not allowed to call cross contract call precompile in static or delegate mode if is_static { - return Err(ExitError::Other(Cow::from(ERR_STATIC))); + return Err(ExitError::Other(Cow::from(consts::ERR_STATIC))); } else if context.address != cross_contract_call::ADDRESS.raw() { - return Err(ExitError::Other(Cow::from(ERR_DELEGATE))); + return Err(ExitError::Other(Cow::from(consts::ERR_DELEGATE))); } let sender = context.caller; let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref()); let args = CrossContractCallArgs::try_from_slice(input) - .map_err(|_| ExitError::Other(Cow::from(ERR_INVALID_INPUT)))?; + .map_err(|_| ExitError::Other(Cow::from(consts::ERR_INVALID_INPUT)))?; let promise = match args { CrossContractCallArgs::Eager(call) => { let call_gas = call.total_gas(); PromiseCreateArgs { target_account_id, - method: ROUTER_EXEC_NAME.into(), + method: consts::ROUTER_EXEC_NAME.into(), args: call .try_to_vec() - .map_err(|_| ExitError::Other(Cow::from(ERR_SERIALIZE)))?, + .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, attached_balance: ZERO_YOCTO, attached_gas: costs::ROUTER_EXEC + call_gas, } } CrossContractCallArgs::Delayed(call) => PromiseCreateArgs { target_account_id, - method: ROUTER_SCHEDULE_NAME.into(), + method: consts::ROUTER_SCHEDULE_NAME.into(), args: call .try_to_vec() - .map_err(|_| ExitError::Other(Cow::from(ERR_SERIALIZE)))?, + .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, attached_balance: ZERO_YOCTO, // We don't need to add any gas to the amount need for the schedule call // since the promise is not executed right away. @@ -118,7 +120,7 @@ impl Precompile for CrossContractCall { topics: Vec::new(), data: promise .try_to_vec() - .map_err(|_| ExitError::Other(Cow::from(ERR_SERIALIZE)))?, + .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, }; Ok(PrecompileOutput { From cc2b9b7a23d337c12d9def462ca041c5572f05ff Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Fri, 5 Aug 2022 07:55:49 -0400 Subject: [PATCH 24/30] Feat(xcc): Estimate EVM gas cost for xcc precompile (#564) --- engine-precompiles/src/xcc.rs | 45 ++++++++++++++--- engine-tests/src/tests/xcc.rs | 95 ++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 10 deletions(-) diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 1afcf9aa7..f4941c948 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -19,8 +19,27 @@ use evm_core::ExitError; pub mod costs { use crate::prelude::types::{EthGas, NearGas}; - // TODO(#483): Determine the correct amount of gas - pub(super) const CROSS_CONTRACT_CALL: EthGas = EthGas::new(0); + /// Base EVM gas cost for calling this precompile. + /// Value obtained from the following methodology: + /// 1. Estimate the cost of calling this precompile in terms of NEAR gas. + /// This is done by calling the precompile with inputs of different lengths + /// and performing a linear regression to obtain a function + /// `NEAR_gas = CROSS_CONTRACT_CALL_BASE + (input_length) * (CROSS_CONTRACT_CALL_BYTE)`. + /// 2. Convert the NEAR gas cost into an EVM gas cost using the conversion ratio below + /// (`CROSS_CONTRACT_CALL_NEAR_GAS`). + /// + /// This process is done in the `test_xcc_eth_gas_cost` test in + /// `engine-tests/src/tests/xcc.rs`. + pub const CROSS_CONTRACT_CALL_BASE: EthGas = EthGas::new(115_000); + /// Additional EVM gas cost per bytes of input given. + /// See `CROSS_CONTRACT_CALL_BASE` for estimation methodology. + pub const CROSS_CONTRACT_CALL_BYTE: EthGas = EthGas::new(2); + /// EVM gas cost per NEAR gas attached to the created promise. + /// This value is derived from the gas report https://hackmd.io/@birchmd/Sy4piXQ29 + /// The units on this quantity are `NEAR Gas / EVM Gas`. + /// The report gives a value `0.175 T(NEAR_gas) / k(EVM_gas)`. To convert the units to + /// `NEAR Gas / EVM Gas`, we simply multiply `0.175 * 10^12 / 10^3 = 175 * 10^6`. + pub const CROSS_CONTRACT_CALL_NEAR_GAS: u64 = 175_000_000; pub const ROUTER_EXEC: NearGas = NearGas::new(7_000_000_000_000); pub const ROUTER_SCHEDULE: NearGas = NearGas::new(5_000_000_000_000); @@ -61,8 +80,10 @@ pub mod cross_contract_call { } impl Precompile for CrossContractCall { - fn required_gas(_input: &[u8]) -> Result { - Ok(costs::CROSS_CONTRACT_CALL) + fn required_gas(input: &[u8]) -> Result { + // This only includes the cost we can easily derive without parsing the input. + // The other cost is added in later to avoid parsing the input more than once. + Ok(costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input.len()) } fn run( @@ -72,11 +93,16 @@ impl Precompile for CrossContractCall { context: &Context, is_static: bool, ) -> EvmPrecompileResult { - if let Some(target_gas) = target_gas { - if Self::required_gas(input)? > target_gas { - return Err(ExitError::OutOfGas); + let mut cost = Self::required_gas(input)?; + let check_cost = |cost: EthGas| -> Result<(), ExitError> { + if let Some(target_gas) = target_gas { + if cost > target_gas { + return Err(ExitError::OutOfGas); + } } - } + Ok(()) + }; + check_cost(cost)?; // It's not allowed to call cross contract call precompile in static or delegate mode if is_static { @@ -114,6 +140,8 @@ impl Precompile for CrossContractCall { attached_gas: costs::ROUTER_SCHEDULE, }, }; + cost += EthGas::new(promise.attached_gas.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); + check_cost(cost)?; let promise_log = Log { address: cross_contract_call::ADDRESS.raw(), @@ -125,6 +153,7 @@ impl Precompile for CrossContractCall { Ok(PrecompileOutput { logs: vec![promise_log], + cost, ..Default::default() } .into()) diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs index 542b7c81a..16fa4b086 100644 --- a/engine-tests/src/tests/xcc.rs +++ b/engine-tests/src/tests/xcc.rs @@ -3,14 +3,105 @@ use crate::tests::erc20_connector::sim_tests; use crate::tests::state_migration::deploy_evm; use aurora_engine_precompiles::xcc::{costs, cross_contract_call}; use aurora_engine_transactions::legacy::TransactionLegacy; -use aurora_engine_types::parameters::{CrossContractCallArgs, PromiseArgs, PromiseCreateArgs}; -use aurora_engine_types::types::{NearGas, Wei, Yocto}; +use aurora_engine_types::parameters::{ + CrossContractCallArgs, PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs, +}; +use aurora_engine_types::types::{Address, EthGas, NearGas, Wei, Yocto}; +use aurora_engine_types::U256; use borsh::BorshSerialize; use near_primitives::transaction::Action; use near_primitives_core::contract::ContractCode; use std::fs; use std::path::Path; +#[test] +fn test_xcc_eth_gas_cost() { + let mut runner = test_utils::deploy_evm(); + runner.standalone_runner = None; + let xcc_wasm_bytes = contract_bytes(); + let _ = runner.call("factory_update", "aurora", xcc_wasm_bytes); + let mut signer = test_utils::Signer::random(); + runner.context.block_index = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT + 1; + + // Baseline transaction that does essentially nothing. + let (_, baseline) = runner + .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { + nonce, + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(Address::from_array([0; 20])), + value: Wei::zero(), + data: Vec::new(), + }) + .unwrap(); + + let mut profile_for_promise = |p: PromiseArgs| -> (u64, u64) { + let data = CrossContractCallArgs::Eager(p).try_to_vec().unwrap(); + let input_length = data.len(); + let (_, profile) = runner + .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { + nonce, + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(cross_contract_call::ADDRESS), + value: Wei::zero(), + data, + }) + .unwrap(); + // Subtract off baseline transaction to isolate just precompile things + ( + u64::try_from(input_length).unwrap(), + profile.all_gas() - baseline.all_gas(), + ) + }; + + let promise = PromiseCreateArgs { + target_account_id: "some_account.near".parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(0), + }; + // Shorter input + let (x1, y1) = profile_for_promise(PromiseArgs::Create(promise.clone())); + // longer input + let (x2, y2) = profile_for_promise(PromiseArgs::Callback(PromiseWithCallbackArgs { + base: promise.clone(), + callback: promise, + })); + + // NEAR costs (inferred from a line through (x1, y1) and (x2, y2)) + let xcc_cost_per_byte = (y2 - y1) / (x2 - x1); + let xcc_base_cost = NearGas::new(y1 - xcc_cost_per_byte * x1); + + // Convert to EVM cost using conversion ratio + let xcc_base_cost = EthGas::new(xcc_base_cost.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); + let xcc_cost_per_byte = xcc_cost_per_byte / costs::CROSS_CONTRACT_CALL_NEAR_GAS; + + let within_5_percent = |a: u64, b: u64| -> bool { + let x = a.max(b); + let y = a.min(b); + + 20 * (x - y) <= x + }; + assert!( + within_5_percent( + xcc_base_cost.as_u64(), + costs::CROSS_CONTRACT_CALL_BASE.as_u64() + ), + "Incorrect xcc base cost. Expected: {} Actual: {}", + xcc_base_cost, + costs::CROSS_CONTRACT_CALL_BASE + ); + + assert!( + within_5_percent(xcc_cost_per_byte, costs::CROSS_CONTRACT_CALL_BYTE.as_u64()), + "Incorrect xcc per byte cost. Expected: {} Actual: {}", + xcc_cost_per_byte, + costs::CROSS_CONTRACT_CALL_BYTE + ); +} + #[test] fn test_xcc_precompile_eager() { test_xcc_precompile_common(false) From f431f5feec6e33953c4e92cd41c97653f0960f69 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Fri, 5 Aug 2022 16:36:36 +0200 Subject: [PATCH 25/30] Post-rebase fixes --- engine-precompiles/src/xcc.rs | 6 ++-- engine-types/src/storage.rs | 1 + etc/xcc-router/Cargo.lock | 57 +++++++++++++++++++++-------------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index f4941c948..beda7d5ff 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -83,7 +83,8 @@ impl Precompile for CrossContractCall { fn required_gas(input: &[u8]) -> Result { // This only includes the cost we can easily derive without parsing the input. // The other cost is added in later to avoid parsing the input more than once. - Ok(costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input.len()) + let input_len = u64::try_from(input.len()).map_err(crate::utils::err_usize_conv)?; + Ok(costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input_len) } fn run( @@ -155,8 +156,7 @@ impl Precompile for CrossContractCall { logs: vec![promise_log], cost, ..Default::default() - } - .into()) + }) } } diff --git a/engine-types/src/storage.rs b/engine-types/src/storage.rs index 294357983..e1b1be281 100644 --- a/engine-types/src/storage.rs +++ b/engine-types/src/storage.rs @@ -46,6 +46,7 @@ impl From for u8 { Generation => 0x7, Nep141Erc20Map => 0x8, Erc20Nep141Map => 0x9, + CrossContractCall => 0xa, } } } diff --git a/etc/xcc-router/Cargo.lock b/etc/xcc-router/Cargo.lock index 27ad8dfa5..d26fc52e0 100644 --- a/etc/xcc-router/Cargo.lock +++ b/etc/xcc-router/Cargo.lock @@ -19,12 +19,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "anyhow" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" - [[package]] name = "arrayref" version = "0.3.6" @@ -50,8 +44,8 @@ dependencies = [ "borsh", "ethabi", "hex", - "primitive-types", - "sha3", + "primitive-types 0.11.1", + "sha3 0.9.1", ] [[package]] @@ -363,21 +357,20 @@ dependencies = [ [[package]] name = "ethabi" -version = "14.1.0" -source = "git+https://github.com/darwinia-network/ethabi?branch=xavier-no-std#09da0834d95f8b43377ca22d7b1c97a2401f4b0c" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f186de076b3e77b8e6d73c99d1b52edc2a229e604f4b5eb6992c06c11d79d537" dependencies = [ - "anyhow", "ethereum-types", "hex", - "sha3", - "uint", + "sha3 0.10.2", ] [[package]] name = "ethbloom" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +checksum = "11da94e443c60508eb62cf256243a64da87304c2802ac2528847f79d750007ef" dependencies = [ "crunchy", "fixed-hash", @@ -386,13 +379,13 @@ dependencies = [ [[package]] name = "ethereum-types" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +checksum = "b2827b94c556145446fcce834ca86b7abf0c39a805883fe20e72c5bfdb5a0dc6" dependencies = [ "ethbloom", "fixed-hash", - "primitive-types", + "primitive-types 0.11.1", "uint", ] @@ -548,7 +541,7 @@ dependencies = [ "near-account-id", "once_cell", "parity-secp256k1", - "primitive-types", + "primitive-types 0.10.1", "rand 0.7.3", "rand_core 0.5.1", "serde", @@ -576,7 +569,7 @@ dependencies = [ "near-vm-errors", "num-rational", "once_cell", - "primitive-types", + "primitive-types 0.10.1", "rand 0.7.3", "reed-solomon-erasure", "serde", @@ -692,8 +685,8 @@ dependencies = [ "near-vm-errors", "ripemd", "serde", - "sha2 0.9.9", - "sha3", + "sha2 0.10.2", + "sha3 0.9.1", ] [[package]] @@ -803,6 +796,16 @@ checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" dependencies = [ "fixed-hash", "impl-codec", + "uint", +] + +[[package]] +name = "primitive-types" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" +dependencies = [ + "fixed-hash", "impl-rlp", "uint", ] @@ -1050,6 +1053,16 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a31480366ec990f395a61b7c08122d99bd40544fdb5abcfc1b06bb29994312c" +dependencies = [ + "digest 0.10.3", + "keccak", +] + [[package]] name = "signature" version = "1.5.0" From a0bf05148abffa8730da643ba9c4baf80db9271b Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 10 Aug 2022 15:42:16 +0200 Subject: [PATCH 26/30] Specify borsh 0.9.3 --- engine-standalone-storage/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-standalone-storage/Cargo.toml b/engine-standalone-storage/Cargo.toml index 26aee6edc..16aefd3e5 100644 --- a/engine-standalone-storage/Cargo.toml +++ b/engine-standalone-storage/Cargo.toml @@ -18,7 +18,7 @@ aurora-engine = { path = "../engine", default-features = false, features = ["std aurora-engine-types = { path = "../engine-types", default-features = false, features = ["std"] } aurora-engine-sdk = { path = "../engine-sdk", default-features = false, features = ["std"] } aurora-engine-transactions = { path = "../engine-transactions", default-features = false, features = ["std"] } -borsh = { version = "0.9" } +borsh = { version = "0.9.3" } evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.36.0-aurora", default-features = false } rocksdb = { version = "0.18.0", default-features = false } postgres = "0.19.2" From 1d51597cda653876b5f195b1f162c8dfd8d720a7 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Tue, 16 Aug 2022 17:16:58 -0400 Subject: [PATCH 27/30] Feat(xcc): User payment using wNEAR ERC-20 (#572) --- engine-precompiles/src/lib.rs | 179 ++++++---- engine-precompiles/src/native.rs | 6 +- engine-precompiles/src/xcc.rs | 282 +++++++++++++-- engine-standalone-storage/src/sync/mod.rs | 5 + engine-standalone-storage/src/sync/types.rs | 8 + engine-tests/src/test_utils/erc20.rs | 28 ++ engine-tests/src/tests/erc20_connector.rs | 4 +- .../src/tests/promise_results_precompile.rs | 2 +- engine-tests/src/tests/res/w_near.wasm | Bin 0 -> 191379 bytes engine-tests/src/tests/xcc.rs | 337 +++++++++++++++--- engine-types/src/parameters.rs | 7 + engine/src/engine.rs | 25 +- engine/src/lib.rs | 11 + engine/src/xcc.rs | 188 +++++++--- etc/xcc-router/Cargo.lock | 2 +- etc/xcc-router/src/lib.rs | 66 +++- etc/xcc-router/src/tests.rs | 8 +- 17 files changed, 935 insertions(+), 223 deletions(-) create mode 100755 engine-tests/src/tests/res/w_near.wasm diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index 4a406debe..78a956e05 100644 --- a/engine-precompiles/src/lib.rs +++ b/engine-precompiles/src/lib.rs @@ -36,9 +36,13 @@ use aurora_engine_sdk::io::IO; use aurora_engine_sdk::promise::ReadOnlyPromiseHandler; use aurora_engine_types::{account_id::AccountId, types::Address, vec, BTreeMap, Box}; use evm::backend::Log; -use evm::executor::{self, stack::PrecompileHandle}; +use evm::executor::{ + self, + stack::{PrecompileFailure, PrecompileHandle}, +}; use evm::{Context, ExitError, ExitSucceed}; use promise_result::PromiseResult; +use xcc::cross_contract_call; #[derive(Debug, Default)] pub struct PrecompileOutput { @@ -76,6 +80,13 @@ pub trait Precompile { ) -> EvmPrecompileResult; } +pub trait HandleBasedPrecompile { + fn run_with_handle( + &self, + handle: &mut impl PrecompileHandle, + ) -> Result; +} + /// Hard fork marker. pub trait HardFork {} @@ -100,15 +111,7 @@ impl HardFork for Istanbul {} impl HardFork for Berlin {} pub struct Precompiles<'a, I, E, H> { - pub generic_precompiles: prelude::BTreeMap>, - // Cannot be part of the generic precompiles because the `dyn` type-erasure messes with - // with the lifetime requirements on the type parameter `I`. - pub near_exit: ExitToNear, - pub ethereum_exit: ExitToEthereum, - pub cross_contract_call: CrossContractCall, - pub predecessor_account_id: PredecessorAccount<'a, E>, - pub prepaid_gas: PrepaidGas<'a, E>, - pub promise_results: PromiseResult, + pub all_precompiles: prelude::BTreeMap>, } impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> executor::stack::PrecompileSet @@ -117,35 +120,58 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> executor::stack::Preco fn execute( &self, handle: &mut impl PrecompileHandle, - ) -> Option> { + ) -> Option> { let address = handle.code_address(); - self.precompile_action(Address::new(address), |p| { - let input = handle.input(); - let gas_limit = handle.gas_limit(); - let context = handle.context(); - let is_static = handle.is_static(); - - match p.run(input, gas_limit.map(EthGas::new), context, is_static) { - Ok(output) => { - handle.record_cost(output.cost.as_u64())?; - for log in output.logs { - handle.log(log.address, log.topics, log.data)?; - } - Ok(executor::stack::PrecompileOutput { - exit_status: ExitSucceed::Returned, - output: output.output, - }) - } - Err(exit_status) => Err(executor::stack::PrecompileFailure::Error { exit_status }), - } - }) + let result = match self.all_precompiles.get(&Address::new(address))? { + AllPrecompiles::ExitToNear(p) => process_precompile(p, handle), + AllPrecompiles::ExitToEthereum(p) => process_precompile(p, handle), + AllPrecompiles::PredecessorAccount(p) => process_precompile(p, handle), + AllPrecompiles::PrepaidGas(p) => process_precompile(p, handle), + AllPrecompiles::PromiseResult(p) => process_precompile(p, handle), + AllPrecompiles::CrossContractCall(p) => process_handle_based_precompile(p, handle), + AllPrecompiles::Generic(p) => process_precompile(p.as_ref(), handle), + }; + Some(result.and_then(|output| post_process(output, handle))) } fn is_precompile(&self, address: prelude::H160) -> bool { - self.precompile_action(Address::new(address), |_| true) - .unwrap_or(false) + self.all_precompiles.contains_key(&Address::new(address)) + } +} + +fn process_precompile( + p: &dyn Precompile, + handle: &mut impl PrecompileHandle, +) -> Result { + let input = handle.input(); + let gas_limit = handle.gas_limit(); + let context = handle.context(); + let is_static = handle.is_static(); + + p.run(input, gas_limit.map(EthGas::new), context, is_static) + .map_err(|exit_status| PrecompileFailure::Error { exit_status }) +} + +fn process_handle_based_precompile( + p: &impl HandleBasedPrecompile, + handle: &mut impl PrecompileHandle, +) -> Result { + p.run_with_handle(handle) +} + +fn post_process( + output: PrecompileOutput, + handle: &mut impl PrecompileHandle, +) -> Result { + handle.record_cost(output.cost.as_u64())?; + for log in output.logs { + handle.log(log.address, log.topics, log.data)?; } + Ok(executor::stack::PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: output.output, + }) } pub struct PrecompileConstructorContext<'a, I, E, H> { @@ -173,7 +199,11 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, Box::new(RandomSeed::new(ctx.random_seed)), Box::new(CurrentAccount::new(ctx.current_account_id.clone())), ]; - let map: BTreeMap> = addresses.into_iter().zip(fun).collect(); + let map = addresses + .into_iter() + .zip(fun) + .map(|(a, f)| (a, AllPrecompiles::Generic(f))) + .collect(); Self::with_generic_precompiles(map, ctx) } @@ -203,7 +233,11 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, Box::new(RandomSeed::new(ctx.random_seed)), Box::new(CurrentAccount::new(ctx.current_account_id.clone())), ]; - let map: BTreeMap> = addresses.into_iter().zip(fun).collect(); + let map = addresses + .into_iter() + .zip(fun) + .map(|(a, f)| (a, AllPrecompiles::Generic(f))) + .collect(); Self::with_generic_precompiles(map, ctx) } @@ -235,7 +269,11 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, Box::new(RandomSeed::new(ctx.random_seed)), Box::new(CurrentAccount::new(ctx.current_account_id.clone())), ]; - let map: BTreeMap> = addresses.into_iter().zip(fun).collect(); + let map = addresses + .into_iter() + .zip(fun) + .map(|(a, f)| (a, AllPrecompiles::Generic(f))) + .collect(); Self::with_generic_precompiles(map, ctx) } @@ -267,7 +305,11 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, Box::new(RandomSeed::new(ctx.random_seed)), Box::new(CurrentAccount::new(ctx.current_account_id.clone())), ]; - let map: BTreeMap> = addresses.into_iter().zip(fun).collect(); + let map = addresses + .into_iter() + .zip(fun) + .map(|(a, f)| (a, AllPrecompiles::Generic(f))) + .collect(); Self::with_generic_precompiles(map, ctx) } @@ -278,7 +320,7 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, } fn with_generic_precompiles( - generic_precompiles: BTreeMap>, + mut generic_precompiles: BTreeMap>, ctx: PrecompileConstructorContext<'a, I, E, H>, ) -> Self { let near_exit = ExitToNear::new(ctx.current_account_id.clone(), ctx.io); @@ -288,39 +330,42 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, let prepaid_gas = PrepaidGas::new(ctx.env); let promise_results = PromiseResult::new(ctx.promise_handler); + generic_precompiles.insert(exit_to_near::ADDRESS, AllPrecompiles::ExitToNear(near_exit)); + generic_precompiles.insert( + exit_to_ethereum::ADDRESS, + AllPrecompiles::ExitToEthereum(ethereum_exit), + ); + generic_precompiles.insert( + cross_contract_call::ADDRESS, + AllPrecompiles::CrossContractCall(cross_contract_call), + ); + generic_precompiles.insert( + predecessor_account::ADDRESS, + AllPrecompiles::PredecessorAccount(predecessor_account_id), + ); + generic_precompiles.insert( + prepaid_gas::ADDRESS, + AllPrecompiles::PrepaidGas(prepaid_gas), + ); + generic_precompiles.insert( + promise_result::ADDRESS, + AllPrecompiles::PromiseResult(promise_results), + ); + Self { - generic_precompiles, - near_exit, - ethereum_exit, - cross_contract_call, - predecessor_account_id, - prepaid_gas, - promise_results, + all_precompiles: generic_precompiles, } } +} - fn precompile_action U>( - &self, - address: Address, - f: F, - ) -> Option { - if address == exit_to_near::ADDRESS { - return Some(f(&self.near_exit)); - } else if address == exit_to_ethereum::ADDRESS { - return Some(f(&self.ethereum_exit)); - } else if address == xcc::cross_contract_call::ADDRESS { - return Some(f(&self.cross_contract_call)); - } else if address == predecessor_account::ADDRESS { - return Some(f(&self.predecessor_account_id)); - } else if address == prepaid_gas::ADDRESS { - return Some(f(&self.prepaid_gas)); - } else if address == promise_result::ADDRESS { - return Some(f(&self.promise_results)); - } - self.generic_precompiles - .get(&address) - .map(|p| f(p.as_ref())) - } +pub enum AllPrecompiles<'a, I, E, H> { + ExitToNear(ExitToNear), + ExitToEthereum(ExitToEthereum), + CrossContractCall(CrossContractCall), + PredecessorAccount(PredecessorAccount<'a, E>), + PrepaidGas(PrepaidGas<'a, E>), + PromiseResult(PromiseResult), + Generic(Box), } /// fn for making an address by concatenating the bytes from two given numbers, diff --git a/engine-precompiles/src/native.rs b/engine-precompiles/src/native.rs index 1eb3342e7..ed3c17699 100644 --- a/engine-precompiles/src/native.rs +++ b/engine-precompiles/src/native.rs @@ -30,12 +30,10 @@ mod costs { // TODO(#483): Determine the correct amount of gas pub(super) const EXIT_TO_ETHEREUM_GAS: EthGas = EthGas::new(0); - // TODO(#332): Determine the correct amount of gas - pub(super) const FT_TRANSFER_GAS: NearGas = NearGas::new(100_000_000_000_000); + pub(super) const FT_TRANSFER_GAS: NearGas = NearGas::new(10_000_000_000_000); - // TODO(#332): Determine the correct amount of gas #[cfg(feature = "error_refund")] - pub(super) const REFUND_ON_ERROR_GAS: NearGas = NearGas::new(60_000_000_000_000); + pub(super) const REFUND_ON_ERROR_GAS: NearGas = NearGas::new(5_000_000_000_000); // TODO(#332): Determine the correct amount of gas pub(super) const WITHDRAWAL_GAS: NearGas = NearGas::new(100_000_000_000_000); diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index beda7d5ff..614b83927 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -3,17 +3,18 @@ //! Allow Aurora users interacting with NEAR smart contracts using cross contract call primitives. //! TODO: How they work (low level explanation with examples) -use crate::{Context, EvmPrecompileResult, Precompile, PrecompileOutput}; +use crate::{HandleBasedPrecompile, PrecompileOutput}; use aurora_engine_sdk::io::IO; use aurora_engine_types::{ account_id::AccountId, format, parameters::{CrossContractCallArgs, PromiseCreateArgs}, - types::{balance::ZERO_YOCTO, EthGas}, - vec, Cow, Vec, H160, + types::{balance::ZERO_YOCTO, Address, EthGas}, + vec, Cow, Vec, H160, H256, U256, }; use borsh::{BorshDeserialize, BorshSerialize}; use evm::backend::Log; +use evm::executor::stack::{PrecompileFailure, PrecompileHandle}; use evm_core::ExitError; pub mod costs { @@ -30,10 +31,10 @@ pub mod costs { /// /// This process is done in the `test_xcc_eth_gas_cost` test in /// `engine-tests/src/tests/xcc.rs`. - pub const CROSS_CONTRACT_CALL_BASE: EthGas = EthGas::new(115_000); + pub const CROSS_CONTRACT_CALL_BASE: EthGas = EthGas::new(323_000); /// Additional EVM gas cost per bytes of input given. /// See `CROSS_CONTRACT_CALL_BASE` for estimation methodology. - pub const CROSS_CONTRACT_CALL_BYTE: EthGas = EthGas::new(2); + pub const CROSS_CONTRACT_CALL_BYTE: EthGas = EthGas::new(3); /// EVM gas cost per NEAR gas attached to the created promise. /// This value is derived from the gas report https://hackmd.io/@birchmd/Sy4piXQ29 /// The units on this quantity are `NEAR Gas / EVM Gas`. @@ -52,6 +53,9 @@ mod consts { pub(super) const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE"; pub(super) const ROUTER_EXEC_NAME: &str = "execute"; pub(super) const ROUTER_SCHEDULE_NAME: &str = "schedule"; + /// Solidity selector for the ERC-20 transferFrom function + /// https://www.4byte.directory/signatures/?bytes4_signature=0x23b872dd + pub(super) const TRANSFER_FROM_SELECTOR: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd]; } pub struct CrossContractCall { @@ -69,7 +73,7 @@ impl CrossContractCall { } pub mod cross_contract_call { - use aurora_engine_types::types::Address; + use aurora_engine_types::{types::Address, H256}; /// Exit to Ethereum precompile address /// @@ -77,28 +81,34 @@ pub mod cross_contract_call { /// This address is computed as: `&keccak("nearCrossContractCall")[12..]` pub const ADDRESS: Address = crate::make_address(0x516cded1, 0xd16af10cad47d6d49128e2eb7d27b372); + + /// Sentinel value used to indicate the following topic field is how much NEAR the + /// cross-contract call will require. + pub const AMOUNT_TOPIC: H256 = + crate::make_h256(0x72657175697265645f6e656172, 0x72657175697265645f6e656172); } -impl Precompile for CrossContractCall { - fn required_gas(input: &[u8]) -> Result { +impl HandleBasedPrecompile for CrossContractCall { + fn run_with_handle( + &self, + handle: &mut impl PrecompileHandle, + ) -> Result { + let input = handle.input(); + let target_gas = handle.gas_limit().map(EthGas::new); + let context = handle.context(); + let is_static = handle.is_static(); + // This only includes the cost we can easily derive without parsing the input. - // The other cost is added in later to avoid parsing the input more than once. + // This allows failing fast without wasting computation on parsing. let input_len = u64::try_from(input.len()).map_err(crate::utils::err_usize_conv)?; - Ok(costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input_len) - } - - fn run( - &self, - input: &[u8], - target_gas: Option, - context: &Context, - is_static: bool, - ) -> EvmPrecompileResult { - let mut cost = Self::required_gas(input)?; - let check_cost = |cost: EthGas| -> Result<(), ExitError> { + let mut cost = + costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input_len; + let check_cost = |cost: EthGas| -> Result<(), PrecompileFailure> { if let Some(target_gas) = target_gas { if cost > target_gas { - return Err(ExitError::OutOfGas); + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }); } } Ok(()) @@ -107,19 +117,20 @@ impl Precompile for CrossContractCall { // It's not allowed to call cross contract call precompile in static or delegate mode if is_static { - return Err(ExitError::Other(Cow::from(consts::ERR_STATIC))); + return Err(revert_with_message(consts::ERR_STATIC)); } else if context.address != cross_contract_call::ADDRESS.raw() { - return Err(ExitError::Other(Cow::from(consts::ERR_DELEGATE))); + return Err(revert_with_message(consts::ERR_DELEGATE)); } let sender = context.caller; let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref()); let args = CrossContractCallArgs::try_from_slice(input) .map_err(|_| ExitError::Other(Cow::from(consts::ERR_INVALID_INPUT)))?; - let promise = match args { + let (promise, attached_near) = match args { CrossContractCallArgs::Eager(call) => { let call_gas = call.total_gas(); - PromiseCreateArgs { + let attached_near = call.total_near(); + let promise = PromiseCreateArgs { target_account_id, method: consts::ROUTER_EXEC_NAME.into(), args: call @@ -127,26 +138,80 @@ impl Precompile for CrossContractCall { .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, attached_balance: ZERO_YOCTO, attached_gas: costs::ROUTER_EXEC + call_gas, - } + }; + (promise, attached_near) + } + CrossContractCallArgs::Delayed(call) => { + let attached_near = call.total_near(); + let promise = PromiseCreateArgs { + target_account_id, + method: consts::ROUTER_SCHEDULE_NAME.into(), + args: call + .try_to_vec() + .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, + attached_balance: ZERO_YOCTO, + // We don't need to add any gas to the amount need for the schedule call + // since the promise is not executed right away. + attached_gas: costs::ROUTER_SCHEDULE, + }; + (promise, attached_near) } - CrossContractCallArgs::Delayed(call) => PromiseCreateArgs { - target_account_id, - method: consts::ROUTER_SCHEDULE_NAME.into(), - args: call - .try_to_vec() - .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, - attached_balance: ZERO_YOCTO, - // We don't need to add any gas to the amount need for the schedule call - // since the promise is not executed right away. - attached_gas: costs::ROUTER_SCHEDULE, - }, }; cost += EthGas::new(promise.attached_gas.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); check_cost(cost)?; + let required_near = + match state::get_code_version_of_address(&self.io, &Address::new(sender)) { + // If there is no deployed version of the router contract then we need to charge for storage staking + None => attached_near + state::STORAGE_AMOUNT, + Some(_) => attached_near, + }; + // if some NEAR payment is needed, transfer it from the caller to the engine's implicit address + if required_near != ZERO_YOCTO { + let engine_implicit_address = aurora_engine_sdk::types::near_account_to_evm_address( + self.engine_account_id.as_bytes(), + ); + let tx_data = transfer_from_args( + sender, + engine_implicit_address.raw(), + U256::from(required_near.as_u128()), + ); + let wnear_address = state::get_wnear_address(&self.io); + let context = evm::Context { + address: wnear_address.raw(), + caller: cross_contract_call::ADDRESS.raw(), + apparent_value: U256::zero(), + }; + let (exit_reason, return_value) = + handle.call(wnear_address.raw(), None, tx_data, None, false, &context); + match exit_reason { + // Transfer successful, nothing to do + evm::ExitReason::Succeed(_) => (), + evm::ExitReason::Revert(r) => { + return Err(PrecompileFailure::Revert { + exit_status: r, + output: return_value, + }) + } + evm::ExitReason::Error(e) => { + return Err(PrecompileFailure::Error { exit_status: e }) + } + evm::ExitReason::Fatal(f) => { + return Err(PrecompileFailure::Fatal { exit_status: f }) + } + }; + } + + let topics = vec![ + cross_contract_call::AMOUNT_TOPIC, + H256(aurora_engine_types::types::u256_to_arr(&U256::from( + required_near.as_u128(), + ))), + ]; + let promise_log = Log { address: cross_contract_call::ADDRESS.raw(), - topics: Vec::new(), + topics, data: promise .try_to_vec() .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, @@ -160,16 +225,102 @@ impl Precompile for CrossContractCall { } } +pub mod state { + //! Functions for reading state related to the cross-contract call feature + + use aurora_engine_sdk::error::ReadU32Error; + use aurora_engine_sdk::io::{StorageIntermediate, IO}; + use aurora_engine_types::storage::{self, KeyPrefix}; + use aurora_engine_types::types::{Address, Yocto}; + use borsh::{BorshDeserialize, BorshSerialize}; + + pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; + pub const ERR_MISSING_WNEAR_ADDRESS: &str = "ERR_MISSING_WNEAR_ADDRESS"; + pub const VERSION_KEY: &[u8] = b"version"; + pub const WNEAR_KEY: &[u8] = b"wnear"; + /// Amount of NEAR needed to cover storage for a router contract. + pub const STORAGE_AMOUNT: Yocto = Yocto::new(2_000_000_000_000_000_000_000_000); + + /// Type wrapper for version of router contracts. + #[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshDeserialize, + BorshSerialize, + )] + pub struct CodeVersion(pub u32); + + impl CodeVersion { + pub fn increment(self) -> Self { + Self(self.0 + 1) + } + } + + /// Get the address of the wNEAR ERC-20 contract + pub fn get_wnear_address(io: &I) -> Address { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, WNEAR_KEY); + match io.read_storage(&key) { + Some(bytes) => Address::try_from_slice(&bytes.to_vec()).expect(ERR_CORRUPTED_STORAGE), + None => panic!("{}", ERR_MISSING_WNEAR_ADDRESS), + } + } + + /// Get the latest router contract version. + pub fn get_latest_code_version(io: &I) -> CodeVersion { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, VERSION_KEY); + read_version(io, &key).unwrap_or_default() + } + + /// Get the version of the currently deploy router for the given address (if it exists). + pub fn get_code_version_of_address(io: &I, address: &Address) -> Option { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, address.as_bytes()); + read_version(io, &key) + } + + /// Private utility method for reading code version from storage. + fn read_version(io: &I, key: &[u8]) -> Option { + match io.read_u32(key) { + Ok(value) => Some(CodeVersion(value)), + Err(ReadU32Error::MissingValue) => None, + Err(ReadU32Error::InvalidU32) => panic!("{}", ERR_CORRUPTED_STORAGE), + } + } +} + +fn transfer_from_args(from: H160, to: H160, amount: U256) -> Vec { + let args = ethabi::encode(&[ + ethabi::Token::Address(from), + ethabi::Token::Address(to), + ethabi::Token::Uint(amount), + ]); + [&consts::TRANSFER_FROM_SELECTOR, args.as_slice()].concat() +} + fn create_target_account_id(sender: H160, engine_account_id: &str) -> AccountId { format!("{}.{}", hex::encode(sender.as_bytes()), engine_account_id) .parse() .unwrap() } +fn revert_with_message(message: &str) -> PrecompileFailure { + PrecompileFailure::Revert { + exit_status: evm::ExitRevert::Reverted, + output: message.as_bytes().to_vec(), + } +} + #[cfg(test)] mod tests { use crate::prelude::sdk::types::near_account_to_evm_address; use crate::xcc::cross_contract_call; + use aurora_engine_types::{vec, H160, U256}; + use rand::Rng; #[test] fn test_precompile_id() { @@ -178,4 +329,57 @@ mod tests { near_account_to_evm_address("nearCrossContractCall".as_bytes()) ); } + + #[test] + fn test_transfer_from_encoding() { + let mut rng = rand::thread_rng(); + let from: [u8; 20] = rng.gen(); + let to: [u8; 20] = rng.gen(); + let amount: [u8; 32] = rng.gen(); + + let from = H160(from); + let to = H160(to); + let amount = U256::from_big_endian(&amount); + + #[allow(deprecated)] + let transfer_from_function = ethabi::Function { + name: "transferFrom".into(), + inputs: vec![ + ethabi::Param { + name: "from".into(), + kind: ethabi::ParamType::Address, + internal_type: None, + }, + ethabi::Param { + name: "to".into(), + kind: ethabi::ParamType::Address, + internal_type: None, + }, + ethabi::Param { + name: "amount".into(), + kind: ethabi::ParamType::Uint(256), + internal_type: None, + }, + ], + outputs: vec![ethabi::Param { + name: "".into(), + kind: ethabi::ParamType::Bool, + internal_type: None, + }], + constant: None, + state_mutability: ethabi::StateMutability::NonPayable, + }; + let expected_tx_data = transfer_from_function + .encode_input(&[ + ethabi::Token::Address(from), + ethabi::Token::Address(to), + ethabi::Token::Uint(amount), + ]) + .unwrap(); + + assert_eq!( + super::transfer_from_args(from, to, amount), + expected_tx_data + ); + } } diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 467064cde..407526e2f 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -380,6 +380,11 @@ fn non_submit_execute<'db>( None } + TransactionKind::FactorySetWNearAddress(address) => { + xcc::set_wnear_address(&mut io, address); + + None + } TransactionKind::Unknown => None, // Not handled in this function; is handled by the general `execute_transaction` function TransactionKind::Submit(_) => unreachable!(), diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index f220753e0..4d732ab50 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -113,6 +113,7 @@ pub enum TransactionKind { FactoryUpdate(Vec), /// Update the version of a deployed xcc-router contract FactoryUpdateAddressVersion(AddressVersionUpdateArgs), + FactorySetWNearAddress(types::Address), /// Sentinel kind for cases where a NEAR receipt caused a /// change in Aurora state, but we failed to parse the Action. Unknown, @@ -241,6 +242,7 @@ enum BorshableTransactionKind<'a> { NewEngine(Cow<'a, parameters::NewCallArgs>), FactoryUpdate(Cow<'a, Vec>), FactoryUpdateAddressVersion(Cow<'a, AddressVersionUpdateArgs>), + FactorySetWNearAddress(types::Address), Unknown, } @@ -276,6 +278,9 @@ impl<'a> From<&'a TransactionKind> for BorshableTransactionKind<'a> { TransactionKind::FactoryUpdateAddressVersion(x) => { Self::FactoryUpdateAddressVersion(Cow::Borrowed(x)) } + TransactionKind::FactorySetWNearAddress(address) => { + Self::FactorySetWNearAddress(*address) + } TransactionKind::Unknown => Self::Unknown, } } @@ -324,6 +329,9 @@ impl<'a> TryFrom> for TransactionKind { BorshableTransactionKind::FactoryUpdateAddressVersion(x) => { Ok(Self::FactoryUpdateAddressVersion(x.into_owned())) } + BorshableTransactionKind::FactorySetWNearAddress(address) => { + Ok(Self::FactorySetWNearAddress(address)) + } BorshableTransactionKind::Unknown => Ok(Self::Unknown), } } diff --git a/engine-tests/src/test_utils/erc20.rs b/engine-tests/src/test_utils/erc20.rs index e6b0aa8e7..be0b8c893 100644 --- a/engine-tests/src/test_utils/erc20.rs +++ b/engine-tests/src/test_utils/erc20.rs @@ -115,6 +115,34 @@ impl ERC20 { } } + pub fn transfer_from( + &self, + from: Address, + to: Address, + amount: U256, + nonce: U256, + ) -> TransactionLegacy { + let data = self + .0 + .abi + .function("transferFrom") + .unwrap() + .encode_input(&[ + ethabi::Token::Address(from.raw()), + ethabi::Token::Address(to.raw()), + ethabi::Token::Uint(amount), + ]) + .unwrap(); + TransactionLegacy { + nonce, + gas_price: Default::default(), + gas_limit: u64::MAX.into(), + to: Some(self.0.address), + value: Default::default(), + data, + } + } + pub fn approve(&self, spender: Address, amount: U256, nonce: U256) -> TransactionLegacy { let data = self .0 diff --git a/engine-tests/src/tests/erc20_connector.rs b/engine-tests/src/tests/erc20_connector.rs index 5689876c8..2e438b15c 100644 --- a/engine-tests/src/tests/erc20_connector.rs +++ b/engine-tests/src/tests/erc20_connector.rs @@ -800,7 +800,7 @@ pub mod sim_tests { .assert_success(); } - fn transfer_nep_141_to_erc_20( + pub(crate) fn transfer_nep_141_to_erc_20( nep_141: &near_sdk_sim::UserAccount, erc20: &ERC20, source: &near_sdk_sim::UserAccount, @@ -865,7 +865,7 @@ pub mod sim_tests { U256::from_big_endian(&test_utils::unwrap_success(submit_result)) } - fn deploy_erc20_from_nep_141( + pub(crate) fn deploy_erc20_from_nep_141( nep_141: &near_sdk_sim::UserAccount, aurora: &AuroraAccount, ) -> ERC20 { diff --git a/engine-tests/src/tests/promise_results_precompile.rs b/engine-tests/src/tests/promise_results_precompile.rs index e6fd09e1f..86388df12 100644 --- a/engine-tests/src/tests/promise_results_precompile.rs +++ b/engine-tests/src/tests/promise_results_precompile.rs @@ -110,7 +110,7 @@ fn test_promise_result_gas_cost() { let x = a.max(b); let y = a.min(b); - 20 * (x - y) <= x + 19 * (x - y) <= x }; assert!( within_5_percent(base_cost.as_u64(), costs::PROMISE_RESULT_BASE_COST.as_u64()), diff --git a/engine-tests/src/tests/res/w_near.wasm b/engine-tests/src/tests/res/w_near.wasm new file mode 100755 index 0000000000000000000000000000000000000000..4c45e3d87b16d0828f81e383f45c5dd4e04f1080 GIT binary patch literal 191379 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 P)N#gABbQTkf&2deUqTVv literal 0 HcmV?d00001 diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs index 16fa4b086..8e4b9a55d 100644 --- a/engine-tests/src/tests/xcc.rs +++ b/engine-tests/src/tests/xcc.rs @@ -1,19 +1,24 @@ +use crate::test_utils::erc20::{ERC20Constructor, ERC20}; use crate::test_utils::{self, AuroraRunner}; use crate::tests::erc20_connector::sim_tests; -use crate::tests::state_migration::deploy_evm; -use aurora_engine_precompiles::xcc::{costs, cross_contract_call}; +use crate::tests::state_migration::{deploy_evm, AuroraAccount}; +use aurora_engine_precompiles::xcc::{self, costs, cross_contract_call}; use aurora_engine_transactions::legacy::TransactionLegacy; use aurora_engine_types::parameters::{ CrossContractCallArgs, PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs, }; use aurora_engine_types::types::{Address, EthGas, NearGas, Wei, Yocto}; use aurora_engine_types::U256; -use borsh::BorshSerialize; +use borsh::{BorshDeserialize, BorshSerialize}; use near_primitives::transaction::Action; use near_primitives_core::contract::ContractCode; +use near_sdk_sim::UserAccount; +use serde_json::json; use std::fs; use std::path::Path; +const WNEAR_AMOUNT: u128 = 10 * near_sdk_sim::STORAGE_AMOUNT; + #[test] fn test_xcc_eth_gas_cost() { let mut runner = test_utils::deploy_evm(); @@ -21,24 +26,49 @@ fn test_xcc_eth_gas_cost() { let xcc_wasm_bytes = contract_bytes(); let _ = runner.call("factory_update", "aurora", xcc_wasm_bytes); let mut signer = test_utils::Signer::random(); + let mut baseline_signer = test_utils::Signer::random(); runner.context.block_index = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT + 1; + // Need to use engine's deploy! + let wnear_erc20 = deploy_erc20(&mut runner, &mut signer); + approve_erc20( + &wnear_erc20, + cross_contract_call::ADDRESS, + &mut runner, + &mut signer, + ); + approve_erc20( + &wnear_erc20, + test_utils::address_from_secret_key(&baseline_signer.secret_key), + &mut runner, + &mut signer, + ); + let _ = runner.call( + "factory_set_wnear_address", + "aurora", + wnear_erc20.0.address.as_bytes().to_vec(), + ); - // Baseline transaction that does essentially nothing. - let (_, baseline) = runner - .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { - nonce, - gas_price: U256::zero(), - gas_limit: u64::MAX.into(), - to: Some(Address::from_array([0; 20])), - value: Wei::zero(), - data: Vec::new(), + // Baseline transaction is an ERC-20 transferFrom call since such a call is included as part + // of the precompile execution, but we want to isolate just the precompile logic itself + // (the EVM subcall is charged separately). + let (baseline_result, baseline) = runner + .submit_with_signer_profiled(&mut baseline_signer, |nonce| { + wnear_erc20.transfer_from( + test_utils::address_from_secret_key(&signer.secret_key), + Address::from_array([1u8; 20]), + U256::from(near_sdk_sim::STORAGE_AMOUNT), + nonce, + ) }) .unwrap(); + if !baseline_result.status.is_ok() { + panic!("Unexpected baseline status: {:?}", baseline_result); + } - let mut profile_for_promise = |p: PromiseArgs| -> (u64, u64) { + let mut profile_for_promise = |p: PromiseArgs| -> (u64, u64, u64) { let data = CrossContractCallArgs::Eager(p).try_to_vec().unwrap(); let input_length = data.len(); - let (_, profile) = runner + let (submit_result, profile) = runner .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { nonce, gas_price: U256::zero(), @@ -48,10 +78,12 @@ fn test_xcc_eth_gas_cost() { data, }) .unwrap(); + assert!(submit_result.status.is_ok()); // Subtract off baseline transaction to isolate just precompile things ( u64::try_from(input_length).unwrap(), profile.all_gas() - baseline.all_gas(), + submit_result.gas_used, ) }; @@ -60,12 +92,12 @@ fn test_xcc_eth_gas_cost() { method: "some_method".into(), args: b"hello_world".to_vec(), attached_balance: Yocto::new(56), - attached_gas: NearGas::new(0), + attached_gas: NearGas::new(500), }; // Shorter input - let (x1, y1) = profile_for_promise(PromiseArgs::Create(promise.clone())); + let (x1, y1, evm1) = profile_for_promise(PromiseArgs::Create(promise.clone())); // longer input - let (x2, y2) = profile_for_promise(PromiseArgs::Callback(PromiseWithCallbackArgs { + let (x2, y2, evm2) = profile_for_promise(PromiseArgs::Callback(PromiseWithCallbackArgs { base: promise.clone(), callback: promise, })); @@ -78,14 +110,15 @@ fn test_xcc_eth_gas_cost() { let xcc_base_cost = EthGas::new(xcc_base_cost.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); let xcc_cost_per_byte = xcc_cost_per_byte / costs::CROSS_CONTRACT_CALL_NEAR_GAS; - let within_5_percent = |a: u64, b: u64| -> bool { - let x = a.max(b); - let y = a.min(b); + let within_x_percent = |x: u64, a: u64, b: u64| -> bool { + let c = a.max(b); + let d = a.min(b); - 20 * (x - y) <= x + (100 / x) * (c - d) <= c }; assert!( - within_5_percent( + within_x_percent( + 5, xcc_base_cost.as_u64(), costs::CROSS_CONTRACT_CALL_BASE.as_u64() ), @@ -95,11 +128,31 @@ fn test_xcc_eth_gas_cost() { ); assert!( - within_5_percent(xcc_cost_per_byte, costs::CROSS_CONTRACT_CALL_BYTE.as_u64()), + within_x_percent( + 5, + xcc_cost_per_byte, + costs::CROSS_CONTRACT_CALL_BYTE.as_u64() + ), "Incorrect xcc per byte cost. Expected: {} Actual: {}", xcc_cost_per_byte, costs::CROSS_CONTRACT_CALL_BYTE ); + + // As a sanity check, confirm that the total EVM gas spent aligns with expectations + let total_gas1 = y1 + baseline.all_gas(); + let total_gas2 = y2 + baseline.all_gas(); + assert!( + within_x_percent(20, evm1, total_gas1 / costs::CROSS_CONTRACT_CALL_NEAR_GAS), + "Incorrect EVM gas used. Expected: {} Actual: {}", + evm1, + total_gas1 / costs::CROSS_CONTRACT_CALL_NEAR_GAS + ); + assert!( + within_x_percent(20, evm2, total_gas2 / costs::CROSS_CONTRACT_CALL_NEAR_GAS), + "Incorrect EVM gas used. Expected: {} Actual: {}", + evm2, + total_gas2 / costs::CROSS_CONTRACT_CALL_NEAR_GAS + ); } #[test] @@ -114,6 +167,7 @@ fn test_xcc_precompile_scheduled() { fn test_xcc_precompile_common(is_scheduled: bool) { let aurora = deploy_evm(); + let chain_id = AuroraRunner::default().chain_id; let xcc_wasm_bytes = contract_bytes(); aurora .user @@ -128,6 +182,46 @@ fn test_xcc_precompile_common(is_scheduled: bool) { let mut signer = test_utils::Signer::random(); let signer_address = test_utils::address_from_secret_key(&signer.secret_key); + + // Setup wNEAR contract and bridge it to Aurora + let wnear_account = deploy_wnear(&aurora); + let wnear_erc20 = sim_tests::deploy_erc20_from_nep_141(&wnear_account, &aurora); + sim_tests::transfer_nep_141_to_erc_20( + &wnear_account, + &wnear_erc20, + &aurora.user, + signer_address, + WNEAR_AMOUNT, + &aurora, + ); + aurora + .user + .call( + aurora.contract.account_id(), + "factory_set_wnear_address", + wnear_erc20.0.address.as_bytes(), + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + let approve_tx = wnear_erc20.approve( + cross_contract_call::ADDRESS, + WNEAR_AMOUNT.into(), + signer.use_nonce().into(), + ); + let signed_transaction = + test_utils::sign_transaction(approve_tx, Some(chain_id), &signer.secret_key); + aurora + .user + .call( + aurora.contract.account_id(), + "submit", + &rlp::encode(&signed_transaction), + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + let router_account = format!( "{}.{}", hex::encode(signer_address.as_bytes()), @@ -210,27 +304,50 @@ fn test_xcc_precompile_common(is_scheduled: bool) { value: Wei::zero(), data: xcc_args.try_to_vec().unwrap(), }; - let signed_transaction = test_utils::sign_transaction( - transaction, - Some(AuroraRunner::default().chain_id), - &signer.secret_key, + let signed_transaction = + test_utils::sign_transaction(transaction, Some(chain_id), &signer.secret_key); + let engine_balance_before_xcc = get_engine_near_balance(&aurora); + let result = aurora.user.call( + aurora.contract.account_id(), + "submit", + &rlp::encode(&signed_transaction), + near_sdk_sim::DEFAULT_GAS, + 0, ); - aurora - .user - .call( - aurora.contract.account_id(), - "submit", - &rlp::encode(&signed_transaction), - near_sdk_sim::DEFAULT_GAS, - 0, - ) - .assert_success(); - - let rt = aurora.user.borrow_runtime(); - for id in rt.last_outcomes.iter() { - println!("{:?}\n\n", rt.outcome(id).unwrap()); + result.assert_success(); + let submit_result: aurora_engine::parameters::SubmitResult = result.unwrap_borsh(); + if !submit_result.status.is_ok() { + panic!("Unexpected result {:?}", submit_result); } - drop(rt); + + print_outcomes(&aurora); + let engine_balance_after_xcc = get_engine_near_balance(&aurora); + assert!( + // engine loses less than 0.01 NEAR + engine_balance_after_xcc.max(engine_balance_before_xcc) + - engine_balance_after_xcc.min(engine_balance_before_xcc) + < 10_000_000_000_000_000_000_000, + "Engine lost too much NEAR funding xcc: Before={:?} After={:?}", + engine_balance_before_xcc, + engine_balance_after_xcc, + ); + let router_balance = aurora + .user + .borrow_runtime() + .view_account(&router_account) + .unwrap() + .amount(); + assert!( + // router loses less than 0.01 NEAR from its allocated funds + xcc::state::STORAGE_AMOUNT.as_u128() - router_balance < 10_000_000_000_000_000_000_000, + "Router lost too much NEAR: Balance={:?}", + router_balance, + ); + // Router has no wNEAR balance because it all was unwrapped to actual NEAR + assert_eq!( + sim_tests::nep_141_balance_of(&router_account, &wnear_account, &aurora), + 0, + ); if is_scheduled { // The promise was only scheduled, not executed immediately. So the FT balance has not changed yet. @@ -258,6 +375,22 @@ fn test_xcc_precompile_common(is_scheduled: bool) { ); } +fn get_engine_near_balance(aurora: &AuroraAccount) -> u128 { + aurora + .user + .borrow_runtime() + .view_account(&aurora.contract.account_id.as_str()) + .unwrap() + .amount() +} + +fn print_outcomes(aurora: &AuroraAccount) { + let rt = aurora.user.borrow_runtime(); + for id in rt.last_outcomes.iter() { + println!("{:?}=={:?}\n\n", id, rt.outcome(id).unwrap()); + } +} + #[test] fn test_xcc_schedule_gas() { let mut router = deploy_router(); @@ -339,7 +472,9 @@ fn deploy_router() -> AuroraRunner { router.context.current_account_id = "some_address.aurora".parse().unwrap(); router.context.predecessor_account_id = "aurora".parse().unwrap(); - let (maybe_outcome, maybe_error) = router.call("initialize", "aurora", Vec::new()); + let init_args = r#"{"wnear_account": "wrap.near", "must_register": true}"#; + let (maybe_outcome, maybe_error) = + router.call("initialize", "aurora", init_args.as_bytes().to_vec()); assert!(maybe_error.is_none()); let outcome = maybe_outcome.unwrap(); assert!(outcome.used_gas < aurora_engine::xcc::INITIALIZE_GAS.as_u64()); @@ -347,6 +482,122 @@ fn deploy_router() -> AuroraRunner { router } +fn deploy_wnear(aurora: &AuroraAccount) -> UserAccount { + let contract_bytes = std::fs::read("src/tests/res/w_near.wasm").unwrap(); + + let account_id = format!("wrap.{}", aurora.user.account_id.as_str()); + let contract_account = aurora.user.deploy( + &contract_bytes, + account_id.parse().unwrap(), + 5 * near_sdk_sim::STORAGE_AMOUNT, + ); + + aurora + .user + .call( + contract_account.account_id(), + "new", + &[], + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + + // Need to register Aurora contract so that it can receive tokens + let args = json!({ + "account_id": &aurora.contract.account_id, + }) + .to_string(); + aurora + .user + .call( + contract_account.account_id(), + "storage_deposit", + args.as_bytes(), + near_sdk_sim::DEFAULT_GAS, + near_sdk_sim::STORAGE_AMOUNT, + ) + .assert_success(); + + // Need to also register root account + let args = json!({ + "account_id": &aurora.user.account_id, + }) + .to_string(); + aurora + .user + .call( + contract_account.account_id(), + "storage_deposit", + args.as_bytes(), + near_sdk_sim::DEFAULT_GAS, + near_sdk_sim::STORAGE_AMOUNT, + ) + .assert_success(); + + // Mint some wNEAR for the root account to use + aurora + .user + .call( + contract_account.account_id(), + "near_deposit", + &[], + near_sdk_sim::DEFAULT_GAS, + WNEAR_AMOUNT, + ) + .assert_success(); + + contract_account +} + +fn deploy_erc20(runner: &mut AuroraRunner, signer: &mut test_utils::Signer) -> ERC20 { + let engine_account = runner.aurora_account_id.clone(); + let args = aurora_engine::parameters::DeployErc20TokenArgs { + nep141: "wrap.near".parse().unwrap(), + }; + let (maybe_output, maybe_error) = runner.call( + "deploy_erc20_token", + &engine_account, + args.try_to_vec().unwrap(), + ); + assert!(maybe_error.is_none()); + let output = maybe_output.unwrap(); + let address = { + let bytes: Vec = + BorshDeserialize::try_from_slice(output.return_data.as_value().as_ref().unwrap()) + .unwrap(); + Address::try_from_slice(&bytes).unwrap() + }; + + let contract = ERC20(ERC20Constructor::load().0.deployed_at(address)); + let dest_address = test_utils::address_from_secret_key(&signer.secret_key); + let call_args = + aurora_engine::parameters::CallArgs::V1(aurora_engine::parameters::FunctionCallArgsV1 { + contract: address, + input: contract + .mint(dest_address, WNEAR_AMOUNT.into(), U256::zero()) + .data, + }); + let (_, maybe_error) = runner.call("call", &engine_account, call_args.try_to_vec().unwrap()); + assert!(maybe_error.is_none()); + + contract +} + +fn approve_erc20( + token: &ERC20, + spender: Address, + runner: &mut AuroraRunner, + signer: &mut test_utils::Signer, +) { + let approve_result = runner + .submit_with_signer(signer, |nonce| { + token.approve(spender, WNEAR_AMOUNT.into(), nonce) + }) + .unwrap(); + assert!(approve_result.status.is_ok()); +} + fn contract_bytes() -> Vec { let base_path = Path::new("../etc").join("xcc-router"); let output_path = base_path.join("target/wasm32-unknown-unknown/release/xcc_router.wasm"); diff --git a/engine-types/src/parameters.rs b/engine-types/src/parameters.rs index 629e612c1..0c78fb1b7 100644 --- a/engine-types/src/parameters.rs +++ b/engine-types/src/parameters.rs @@ -17,6 +17,13 @@ impl PromiseArgs { Self::Callback(cb) => cb.base.attached_gas + cb.callback.attached_gas, } } + + pub fn total_near(&self) -> Yocto { + match self { + Self::Create(call) => call.attached_balance, + Self::Callback(cb) => cb.base.attached_balance + cb.callback.attached_balance, + } + } } #[must_use] diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 59980c69f..e961d2e14 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -22,7 +22,7 @@ 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, ERC20_MINT_SELECTOR, H160, H256, U256, + ToString, Vec, Wei, Yocto, ERC20_MINT_SELECTOR, H160, H256, U256, }; use aurora_engine_precompiles::PrecompileConstructorContext; use core::cell::RefCell; @@ -243,7 +243,7 @@ impl AsRef<[u8]> for DeployErc20Error { } } -pub struct ERC20Address(Address); +pub struct ERC20Address(pub Address); impl AsRef<[u8]> for ERC20Address { fn as_ref(&self) -> &[u8] { @@ -273,7 +273,7 @@ impl AsRef<[u8]> for AddressParseError { } } -pub struct NEP141Account(AccountId); +pub struct NEP141Account(pub AccountId); impl AsRef<[u8]> for NEP141Account { fn as_ref(&self) -> &[u8] { @@ -1366,7 +1366,7 @@ fn filter_promises_from_logs( where T: IntoIterator, P: PromiseHandler, - I: IO, + I: IO + Copy, { logs.into_iter() .filter_map(|log| { @@ -1392,8 +1392,21 @@ where Some(log.into()) } } else if log.address == cross_contract_call::ADDRESS.raw() { - if let Ok(promise) = PromiseCreateArgs::try_from_slice(&log.data) { - crate::xcc::handle_precompile_promise(io, handler, promise, current_account_id); + if log.topics[0] == 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]); + let required_near = + Yocto::new(U256::from_big_endian(log.topics[1].as_bytes()).low_u128()); + if let Ok(promise) = PromiseCreateArgs::try_from_slice(&log.data) { + crate::xcc::handle_precompile_promise( + io, + handler, + promise, + required_near, + current_account_id, + ); + } } // do not pass on these "internal logs" to caller None diff --git a/engine/src/lib.rs b/engine/src/lib.rs index d2c130732..96be012ff 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -306,6 +306,17 @@ mod contract { crate::xcc::set_code_version_of_address(&mut io, &args.address, args.version); } + /// Sets the address for the wNEAR ERC-20 contract. This contract will be used by the + /// cross-contract calls feature to have users pay for their NEAR transactions. + #[no_mangle] + pub extern "C" fn factory_set_wnear_address() { + let mut io = Runtime; + let state = engine::get_state(&io).sdk_unwrap(); + require_owner_only(&state, &io.predecessor_account_id()); + let address = io.read_input_arr20().sdk_unwrap(); + crate::xcc::set_wnear_address(&mut io, &Address::from_array(address)); + } + /// Allow receiving NEP141 tokens to the EVM contract. /// /// This function returns the amount of tokens to return to the sender. diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index fc7130f7e..564dae3a0 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -1,36 +1,28 @@ -use aurora_engine_sdk::error::ReadU32Error; +use crate::parameters::{CallArgs, FunctionCallArgsV2}; +use aurora_engine_precompiles::xcc::state; use aurora_engine_sdk::io::{StorageIntermediate, IO}; use aurora_engine_sdk::promise::PromiseHandler; use aurora_engine_types::account_id::AccountId; use aurora_engine_types::parameters::{PromiseAction, PromiseBatchAction, PromiseCreateArgs}; use aurora_engine_types::storage::{self, KeyPrefix}; use aurora_engine_types::types::{Address, NearGas, Yocto, ZERO_YOCTO}; -use aurora_engine_types::Cow; -use aurora_engine_types::Vec; +use aurora_engine_types::{format, Cow, Vec, U256}; use borsh::{BorshDeserialize, BorshSerialize}; pub const ERR_NO_ROUTER_CODE: &str = "ERR_MISSING_XCC_BYTECODE"; -pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE"; pub const ERR_INVALID_ACCOUNT: &str = "ERR_INVALID_XCC_ACCOUNT"; pub const ERR_ATTACHED_NEAR: &str = "ERR_ATTACHED_XCC_NEAR"; -pub const VERSION_KEY: &[u8] = b"version"; pub const CODE_KEY: &[u8] = b"router_code"; pub const VERSION_UPDATE_GAS: NearGas = NearGas::new(5_000_000_000_000); -pub const INITIALIZE_GAS: NearGas = NearGas::new(5_000_000_000_000); -/// Amount of NEAR needed to cover storage for a router contract. -pub const STORAGE_AMOUNT: Yocto = Yocto::new(2_000_000_000_000_000_000_000_000); - -/// Type wrapper for version of router contracts. -#[derive( - Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, BorshDeserialize, BorshSerialize, -)] -pub struct CodeVersion(pub u32); - -impl CodeVersion { - pub fn increment(self) -> Self { - Self(self.0 + 1) - } -} +pub const INITIALIZE_GAS: NearGas = NearGas::new(15_000_000_000_000); +pub const UNWRAP_AND_REFUND_GAS: NearGas = NearGas::new(25_000_000_000_000); +pub const WITHDRAW_GAS: NearGas = NearGas::new(30_000_000_000_000); +pub const WITHDRAW_TO_NEAR_SELECTOR: [u8; 4] = [0x6b, 0x35, 0x18, 0x48]; + +pub use aurora_engine_precompiles::xcc::state::{ + get_code_version_of_address, get_latest_code_version, get_wnear_address, CodeVersion, + ERR_CORRUPTED_STORAGE, STORAGE_AMOUNT, VERSION_KEY, WNEAR_KEY, +}; /// Type wrapper for router bytecode. #[derive(Debug, Clone, PartialEq, Eq)] @@ -56,10 +48,11 @@ pub fn handle_precompile_promise( io: &I, handler: &mut P, promise: PromiseCreateArgs, + required_near: Yocto, current_account_id: &AccountId, ) where P: PromiseHandler, - I: IO, + I: IO + Copy, { let target_account: &str = promise.target_account_id.as_ref(); let sender = Address::decode(&target_account[0..40]).expect(ERR_INVALID_ACCOUNT); @@ -92,10 +85,13 @@ pub fn handle_precompile_promise( }, Some(_version) => AddressVersionStatus::UpToDate, }; - let _promise_id = match deploy_needed { + // 1. If the router contract account does not exist or is out of date then we start + // with a batch transaction to deploy the router. This batch also has an attached + // callback to update the engine's storage with the new version of that router account. + let setup_id = match &deploy_needed { AddressVersionStatus::DeployNeeded { create_needed } => { let mut promise_actions = Vec::with_capacity(4); - if create_needed { + if *create_needed { promise_actions.push(PromiseAction::CreateAccount); promise_actions.push(PromiseAction::Transfer { amount: STORAGE_AMOUNT, @@ -105,25 +101,26 @@ pub fn handle_precompile_promise( code: get_router_code(io).0.into_owned(), }); // After a deploy we call the contract's initialize function + let wnear_address = state::get_wnear_address(io); + let wnear_account = crate::engine::nep141_erc20_map(*io) + .lookup_right(&crate::engine::ERC20Address(wnear_address)) + .unwrap(); + let init_args = format!( + r#"{{"wnear_account": "{}", "must_register": {}}}"#, + wnear_account.0.as_ref(), + create_needed, + ); promise_actions.push(PromiseAction::FunctionCall { name: "initialize".into(), - args: Vec::new(), + args: init_args.into_bytes(), attached_yocto: ZERO_YOCTO, gas: INITIALIZE_GAS, }); - // After the contract is deployed and initialized, we can call the method requested - promise_actions.push(PromiseAction::FunctionCall { - name: promise.method, - args: promise.args, - attached_yocto: promise.attached_balance, - gas: promise.attached_gas, - }); let batch = PromiseBatchAction { - target_account_id: promise.target_account_id, + target_account_id: promise.target_account_id.clone(), actions: promise_actions, }; let promise_id = handler.promise_create_batch(&batch); - // Add a callback here to update the version of the account let args = AddressVersionUpdateArgs { address: sender, @@ -137,9 +134,58 @@ pub fn handle_precompile_promise( attached_gas: VERSION_UPDATE_GAS, }; - handler.promise_attach_callback(promise_id, &callback) + Some(handler.promise_attach_callback(promise_id, &callback)) } - AddressVersionStatus::UpToDate => handler.promise_create_call(&promise), + AddressVersionStatus::UpToDate => None, + }; + // 2. If some NEAR is required for this call (from storage staking for a new account + // and/or attached NEAR to the call the user wants to make), then we need to have the + // engine withdraw that amount of wNEAR to the router account and then have the router + // unwrap it into actual NEAR. In the case of storage staking, the engine contract + // covered the cost initially (see setup batch above), so the unwrapping also sends + // a refund back to the engine. + let withdraw_id = if required_near == ZERO_YOCTO { + setup_id + } else { + let wnear_address = state::get_wnear_address(io); + let withdraw_call_args = CallArgs::V2(FunctionCallArgsV2 { + contract: wnear_address, + value: [0u8; 32], + input: withdraw_to_near_args(&promise.target_account_id, required_near), + }); + let withdraw_call = PromiseCreateArgs { + target_account_id: current_account_id.clone(), + method: "call".into(), + args: withdraw_call_args.try_to_vec().unwrap(), + attached_balance: ZERO_YOCTO, + attached_gas: WITHDRAW_GAS, + }; + let id = match setup_id { + None => handler.promise_create_call(&withdraw_call), + Some(setup_id) => handler.promise_attach_callback(setup_id, &withdraw_call), + }; + let refund_needed = match deploy_needed { + AddressVersionStatus::DeployNeeded { create_needed } => create_needed, + AddressVersionStatus::UpToDate => false, + }; + let args = format!( + r#"{{"amount": "{}", "refund_needed": {}}}"#, + required_near.as_u128(), + refund_needed, + ); + let unwrap_call = PromiseCreateArgs { + target_account_id: promise.target_account_id.clone(), + method: "unwrap_and_refund_storage".into(), + args: args.into_bytes(), + attached_balance: ZERO_YOCTO, + attached_gas: UNWRAP_AND_REFUND_GAS, + }; + Some(handler.promise_attach_callback(id, &unwrap_call)) + }; + // 3. Finally we can do the call the user wanted to do. + let _promise_id = match withdraw_id { + None => handler.promise_create_call(&promise), + Some(withdraw_id) => handler.promise_attach_callback(withdraw_id, &promise), }; } @@ -159,16 +205,10 @@ pub fn update_router_code(io: &mut I, code: &RouterCode) { set_latest_code_version(io, current_version.increment()); } -/// Get the latest router contract version. -pub fn get_latest_code_version(io: &I) -> CodeVersion { - let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, VERSION_KEY); - read_version(io, &key).unwrap_or_default() -} - -/// Get the version of the currently deploy router for the given address (if it exists). -pub fn get_code_version_of_address(io: &I, address: &Address) -> Option { - let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, address.as_bytes()); - read_version(io, &key) +/// Set the address of the wNEAR ERC-20 contract +pub fn set_wnear_address(io: &mut I, address: &Address) { + let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, WNEAR_KEY); + io.write_storage(&key, address.as_bytes()); } /// Set the version of the router contract deployed for the given address. @@ -186,17 +226,57 @@ fn set_latest_code_version(io: &mut I, version: CodeVersion) { io.write_storage(&key, &value_bytes); } -/// Private utility method for reading code version from storage. -fn read_version(io: &I, key: &[u8]) -> Option { - match io.read_u32(key) { - Ok(value) => Some(CodeVersion(value)), - Err(ReadU32Error::MissingValue) => None, - Err(ReadU32Error::InvalidU32) => panic!("{}", ERR_CORRUPTED_STORAGE), - } -} - /// Private enum used for bookkeeping what actions are needed in the call to the router contract. enum AddressVersionStatus { UpToDate, DeployNeeded { create_needed: bool }, } + +fn withdraw_to_near_args(recipient: &AccountId, amount: Yocto) -> Vec { + let args = ethabi::encode(&[ + ethabi::Token::Bytes(recipient.as_bytes().to_vec()), + ethabi::Token::Uint(U256::from(amount.as_u128())), + ]); + [&WITHDRAW_TO_NEAR_SELECTOR, args.as_slice()].concat() +} + +#[cfg(test)] +mod tests { + use aurora_engine_types::{account_id::AccountId, types::Yocto, U256}; + + #[test] + fn test_withdraw_to_near_encoding() { + let recipient: AccountId = "some_account.near".parse().unwrap(); + let amount = Yocto::new(1332654); + #[allow(deprecated)] + let withdraw_function = ethabi::Function { + name: "withdrawToNear".into(), + inputs: vec![ + ethabi::Param { + name: "recipient".into(), + kind: ethabi::ParamType::Bytes, + internal_type: None, + }, + ethabi::Param { + name: "amount".into(), + kind: ethabi::ParamType::Uint(256), + internal_type: None, + }, + ], + outputs: vec![], + constant: None, + state_mutability: ethabi::StateMutability::NonPayable, + }; + let expected_tx_data = withdraw_function + .encode_input(&[ + ethabi::Token::Bytes(recipient.as_bytes().to_vec()), + ethabi::Token::Uint(U256::from(amount.as_u128())), + ]) + .unwrap(); + + assert_eq!( + super::withdraw_to_near_args(&recipient, amount), + expected_tx_data + ); + } +} diff --git a/etc/xcc-router/Cargo.lock b/etc/xcc-router/Cargo.lock index d26fc52e0..bf5f81964 100644 --- a/etc/xcc-router/Cargo.lock +++ b/etc/xcc-router/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "ethabi", "hex", "primitive-types 0.11.1", - "sha3 0.9.1", + "sha3 0.10.2", ] [[package]] diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index 07beef4b5..1e6308b98 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -1,9 +1,9 @@ use aurora_engine_types::parameters::{PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs}; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; -use near_sdk::json_types::U64; +use near_sdk::json_types::{U128, U64}; use near_sdk::BorshStorageKey; -use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault, PromiseIndex}; +use near_sdk::{env, near_bindgen, AccountId, Gas, PanicOnDefault, Promise, PromiseIndex}; #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] @@ -20,6 +20,12 @@ enum StorageKey { const CURRENT_VERSION: u32 = 1; const ERR_ILLEGAL_CALLER: &str = "ERR_ILLEGAL_CALLER"; +const WNEAR_WITHDRAW_GAS: Gas = Gas(5_000_000_000_000); +const WNEAR_REGISTER_GAS: Gas = Gas(5_000_000_000_000); +const REFUND_GAS: Gas = Gas(5_000_000_000_000); +const WNEAR_REGISTER_AMOUNT: u128 = 1_250_000_000_000_000_000_000; +/// Must match arora_engine_precompiles::xcc::state::STORAGE_AMOUNT +const REFUND_AMOUNT: u128 = 2_000_000_000_000_000_000_000_000; #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] @@ -33,12 +39,14 @@ pub struct Router { nonce: LazyOption, /// The storage for the scheduled promises. scheduled_promises: LookupMap, + /// Account ID for the wNEAR contract. + wnear_account: AccountId, } #[near_bindgen] impl Router { #[init(ignore_state)] - pub fn initialize() -> Self { + pub fn initialize(wnear_account: AccountId, must_register: bool) -> Self { // The first time this function is called there is no state and the parent is set to be // the predecessor account id. In subsequent calls, only the original parent is allowed to // call this function. The idea is that the Create, Deploy and Initialize actions are done in a single @@ -59,6 +67,16 @@ impl Router { } } + if must_register { + env::promise_create( + wnear_account.clone(), + "storage_deposit", + b"{}", + WNEAR_REGISTER_AMOUNT, + WNEAR_REGISTER_GAS, + ); + } + let mut version = LazyOption::new(StorageKey::Version, None); if version.get().unwrap_or_default() != CURRENT_VERSION { // Future migrations would go here @@ -73,6 +91,7 @@ impl Router { version, nonce, scheduled_promises, + wnear_account, } } @@ -112,6 +131,47 @@ impl Router { let promise_id = Router::promise_create(promise); env::promise_return(promise_id) } + + /// The router will receive wNEAR deposits from its user. This function is to + /// unwrap that wNEAR into NEAR. Additionally, this function will transfer some + /// NEAR back to its parent, if needed. This transfer is done because the parent + /// must cover the storage staking cost with the router account is first created, + /// but the user ultimately is responsible to pay for it. + pub fn unwrap_and_refund_storage(&self, amount: U128, refund_needed: bool) { + self.require_parent_caller(); + + let args = format!(r#"{{"amount": "{}"}}"#, amount.0); + let id = env::promise_create( + self.wnear_account.clone(), + "near_withdraw", + args.as_bytes(), + 1, + WNEAR_WITHDRAW_GAS, + ); + let final_id = if refund_needed { + env::promise_then( + id, + env::current_account_id(), + "send_refund", + &[], + 0, + REFUND_GAS, + ) + } else { + id + }; + env::promise_return(final_id); + } + + #[private] + pub fn send_refund(&self) -> Promise { + let parent = self + .parent + .get() + .unwrap_or_else(|| env::panic_str("ERR_CONTRACT_NOT_INITIALIZED")); + + Promise::new(parent).transfer(REFUND_AMOUNT) + } } impl Router { diff --git a/etc/xcc-router/src/tests.rs b/etc/xcc-router/src/tests.rs index 586c6a661..ab9ee2a04 100644 --- a/etc/xcc-router/src/tests.rs +++ b/etc/xcc-router/src/tests.rs @@ -6,6 +6,8 @@ use near_sdk::test_utils::test_env::{alice, bob, carol}; use near_sdk::test_utils::{self, VMContextBuilder}; use near_sdk::testing_env; +const WNEAR_ACCOUNT: &str = "wrap.near"; + #[test] fn test_initialize() { let (parent, contract) = create_contract(); @@ -22,7 +24,7 @@ fn test_reinitialize() { contract.nonce.set(&nonce); drop(contract); - let contract = Router::initialize(); + let contract = Router::initialize(WNEAR_ACCOUNT.parse().unwrap(), false); assert_eq!(contract.nonce.get().unwrap(), nonce); } @@ -38,7 +40,7 @@ fn test_reinitialize_wrong_caller() { testing_env!(VMContextBuilder::new() .predecessor_account_id(bob()) .build()); - let _contract = Router::initialize(); + let _contract = Router::initialize(WNEAR_ACCOUNT.parse().unwrap(), false); } #[test] @@ -203,7 +205,7 @@ fn create_contract() -> (near_sdk::AccountId, Router) { .current_account_id(format!("some_address.{}", parent).try_into().unwrap()) .predecessor_account_id(parent.clone()) .build()); - let contract = Router::initialize(); + let contract = Router::initialize(WNEAR_ACCOUNT.parse().unwrap(), false); (parent, contract) } From 8643c3d343768ca2e4f6f3f70850bf9d46c062ff Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 17 Aug 2022 15:27:30 +0200 Subject: [PATCH 28/30] Factor out within_x_percent to test_utils to reuse it in both precompile gas estimate tests --- engine-tests/src/test_utils/mod.rs | 8 ++++++++ .../src/tests/promise_results_precompile.rs | 18 ++++++++---------- engine-tests/src/tests/xcc.rs | 14 ++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/engine-tests/src/test_utils/mod.rs b/engine-tests/src/test_utils/mod.rs index b9d63d727..6ed7188a4 100644 --- a/engine-tests/src/test_utils/mod.rs +++ b/engine-tests/src/test_utils/mod.rs @@ -861,3 +861,11 @@ pub fn assert_gas_bound(total_gas: u64, tgas_bound: u64) { tgas_bound, ); } + +/// Returns true if `abs(a - b) / max(a, b) <= x / 100`. The implementation is written differently than +/// this simpler formula to avoid floating point arithmetic. +pub fn within_x_percent(x: u64, a: u64, b: u64) -> bool { + let (larger, smaller) = if a < b { (b, a) } else { (a, b) }; + + (100 / x) * (larger - smaller) <= larger +} diff --git a/engine-tests/src/tests/promise_results_precompile.rs b/engine-tests/src/tests/promise_results_precompile.rs index 86388df12..f85afbcbe 100644 --- a/engine-tests/src/tests/promise_results_precompile.rs +++ b/engine-tests/src/tests/promise_results_precompile.rs @@ -106,21 +106,19 @@ fn test_promise_result_gas_cost() { let base_cost = EthGas::new(base_cost.as_u64() / NEAR_GAS_PER_EVM); let cost_per_byte = cost_per_byte / NEAR_GAS_PER_EVM; - let within_5_percent = |a: u64, b: u64| -> bool { - let x = a.max(b); - let y = a.min(b); - - 19 * (x - y) <= x - }; assert!( - within_5_percent(base_cost.as_u64(), costs::PROMISE_RESULT_BASE_COST.as_u64()), + test_utils::within_x_percent( + 5, + base_cost.as_u64(), + costs::PROMISE_RESULT_BASE_COST.as_u64() + ), "Incorrect promise_result base cost. Expected: {} Actual: {}", base_cost, costs::PROMISE_RESULT_BASE_COST ); assert!( - within_5_percent(cost_per_byte, costs::PROMISE_RESULT_BYTE_COST.as_u64()), + test_utils::within_x_percent(5, cost_per_byte, costs::PROMISE_RESULT_BYTE_COST.as_u64()), "Incorrect promise_result per byte cost. Expected: {} Actual: {}", cost_per_byte, costs::PROMISE_RESULT_BYTE_COST @@ -129,13 +127,13 @@ fn test_promise_result_gas_cost() { let total_gas1 = y1 + baseline.all_gas(); let total_gas2 = y2 + baseline.all_gas(); assert!( - within_5_percent(evm1, total_gas1 / NEAR_GAS_PER_EVM), + test_utils::within_x_percent(6, evm1, total_gas1 / NEAR_GAS_PER_EVM), "Incorrect EVM gas used. Expected: {} Actual: {}", evm1, total_gas1 / NEAR_GAS_PER_EVM ); assert!( - within_5_percent(evm2, total_gas2 / NEAR_GAS_PER_EVM), + test_utils::within_x_percent(6, evm2, total_gas2 / NEAR_GAS_PER_EVM), "Incorrect EVM gas used. Expected: {} Actual: {}", evm2, total_gas2 / NEAR_GAS_PER_EVM diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs index 8e4b9a55d..6f9bbad15 100644 --- a/engine-tests/src/tests/xcc.rs +++ b/engine-tests/src/tests/xcc.rs @@ -110,14 +110,8 @@ fn test_xcc_eth_gas_cost() { let xcc_base_cost = EthGas::new(xcc_base_cost.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); let xcc_cost_per_byte = xcc_cost_per_byte / costs::CROSS_CONTRACT_CALL_NEAR_GAS; - let within_x_percent = |x: u64, a: u64, b: u64| -> bool { - let c = a.max(b); - let d = a.min(b); - - (100 / x) * (c - d) <= c - }; assert!( - within_x_percent( + test_utils::within_x_percent( 5, xcc_base_cost.as_u64(), costs::CROSS_CONTRACT_CALL_BASE.as_u64() @@ -128,7 +122,7 @@ fn test_xcc_eth_gas_cost() { ); assert!( - within_x_percent( + test_utils::within_x_percent( 5, xcc_cost_per_byte, costs::CROSS_CONTRACT_CALL_BYTE.as_u64() @@ -142,13 +136,13 @@ fn test_xcc_eth_gas_cost() { let total_gas1 = y1 + baseline.all_gas(); let total_gas2 = y2 + baseline.all_gas(); assert!( - within_x_percent(20, evm1, total_gas1 / costs::CROSS_CONTRACT_CALL_NEAR_GAS), + test_utils::within_x_percent(20, evm1, total_gas1 / costs::CROSS_CONTRACT_CALL_NEAR_GAS), "Incorrect EVM gas used. Expected: {} Actual: {}", evm1, total_gas1 / costs::CROSS_CONTRACT_CALL_NEAR_GAS ); assert!( - within_x_percent(20, evm2, total_gas2 / costs::CROSS_CONTRACT_CALL_NEAR_GAS), + test_utils::within_x_percent(20, evm2, total_gas2 / costs::CROSS_CONTRACT_CALL_NEAR_GAS), "Incorrect EVM gas used. Expected: {} Actual: {}", evm2, total_gas2 / costs::CROSS_CONTRACT_CALL_NEAR_GAS From 0a9121c1b7a5a49875c870e884617c02c990e3ca Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Wed, 17 Aug 2022 15:51:06 +0200 Subject: [PATCH 29/30] Do not enable the xcc precompile on mainnet yet --- engine/src/engine.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/engine/src/engine.rs b/engine/src/engine.rs index e961d2e14..7172881fc 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -362,14 +362,30 @@ impl<'env, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> StackExecutorParams< env: &'env E, ro_promise_handler: H, ) -> Self { - Self { - precompiles: Precompiles::new_london(PrecompileConstructorContext { + let precompiles = if cfg!(all(feature = "mainnet", not(feature = "integration-test"))) { + let mut tmp = Precompiles::new_london(PrecompileConstructorContext { current_account_id, random_seed, io, env, 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 + } else { + Precompiles::new_london(PrecompileConstructorContext { + current_account_id, + random_seed, + io, + env, + promise_handler: ro_promise_handler, + }) + }; + + Self { + precompiles, gas_limit, } } From c2ec976ca623c2df033afb815b36d310e70a7652 Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Thu, 18 Aug 2022 19:53:11 +0200 Subject: [PATCH 30/30] Post-rebase fixes --- Cargo.lock | 2 +- engine-precompiles/src/xcc.rs | 2 +- etc/xcc-router/Cargo.lock | 57 +---------------------------------- 3 files changed, 3 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66bd6dce6..7ca379e96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4826,7 +4826,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e61de68ede9ffdd69c01664f65a178c5188b73f78faa21f0936016a888ff7c" dependencies = [ - "borsh 0.9.3", + "borsh", "byteorder", "crunchy", "lazy_static", diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 614b83927..e8e009ad2 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -15,7 +15,7 @@ use aurora_engine_types::{ use borsh::{BorshDeserialize, BorshSerialize}; use evm::backend::Log; use evm::executor::stack::{PrecompileFailure, PrecompileHandle}; -use evm_core::ExitError; +use evm::ExitError; pub mod costs { use crate::prelude::types::{EthGas, NearGas}; diff --git a/etc/xcc-router/Cargo.lock b/etc/xcc-router/Cargo.lock index bf5f81964..615e1bbef 100644 --- a/etc/xcc-router/Cargo.lock +++ b/etc/xcc-router/Cargo.lock @@ -42,10 +42,8 @@ name = "aurora-engine-types" version = "1.0.0" dependencies = [ "borsh", - "ethabi", "hex", "primitive-types 0.11.1", - "sha3 0.10.2", ] [[package]] @@ -355,40 +353,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ethabi" -version = "17.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f186de076b3e77b8e6d73c99d1b52edc2a229e604f4b5eb6992c06c11d79d537" -dependencies = [ - "ethereum-types", - "hex", - "sha3 0.10.2", -] - -[[package]] -name = "ethbloom" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11da94e443c60508eb62cf256243a64da87304c2802ac2528847f79d750007ef" -dependencies = [ - "crunchy", - "fixed-hash", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2827b94c556145446fcce834ca86b7abf0c39a805883fe20e72c5bfdb5a0dc6" -dependencies = [ - "ethbloom", - "fixed-hash", - "primitive-types 0.11.1", - "uint", -] - [[package]] name = "fixed-hash" version = "0.7.0" @@ -686,7 +650,7 @@ dependencies = [ "ripemd", "serde", "sha2 0.10.2", - "sha3 0.9.1", + "sha3", ] [[package]] @@ -1053,16 +1017,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "sha3" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a31480366ec990f395a61b7c08122d99bd40544fdb5abcfc1b06bb29994312c" -dependencies = [ - "digest 0.10.3", - "keccak", -] - [[package]] name = "signature" version = "1.5.0" @@ -1180,15 +1134,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "toml" version = "0.5.9"