diff --git a/Cargo.lock b/Cargo.lock index 51d22d5c9..7ca379e96 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", @@ -4886,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/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/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-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index 10ab661e7..78a956e05 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,14 +30,19 @@ 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; 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 { @@ -74,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 {} @@ -98,14 +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 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 @@ -114,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> { @@ -170,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) } @@ -200,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) } @@ -232,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) } @@ -264,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) } @@ -275,45 +320,52 @@ 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); - let ethereum_exit = ExitToEthereum::new(ctx.current_account_id, 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); + 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, - 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 == 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 new file mode 100644 index 000000000..e8e009ad2 --- /dev/null +++ b/engine-precompiles/src/xcc.rs @@ -0,0 +1,385 @@ +//! 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::{HandleBasedPrecompile, PrecompileOutput}; +use aurora_engine_sdk::io::IO; +use aurora_engine_types::{ + account_id::AccountId, + format, + parameters::{CrossContractCallArgs, PromiseCreateArgs}, + 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::ExitError; + +pub mod costs { + use crate::prelude::types::{EthGas, NearGas}; + + /// 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(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(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`. + /// 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); +} + +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"; + /// 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 { + io: I, + engine_account_id: AccountId, +} + +impl CrossContractCall { + pub fn new(engine_account_id: AccountId, io: I) -> Self { + Self { + io, + engine_account_id, + } + } +} + +pub mod cross_contract_call { + use aurora_engine_types::{types::Address, H256}; + + /// Exit to Ethereum precompile address + /// + /// Address: `0x516cded1d16af10cad47d6d49128e2eb7d27b372` + /// 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 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. + // This allows failing fast without wasting computation on parsing. + let input_len = u64::try_from(input.len()).map_err(crate::utils::err_usize_conv)?; + 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(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }); + } + } + Ok(()) + }; + check_cost(cost)?; + + // It's not allowed to call cross contract call precompile in static or delegate mode + if is_static { + return Err(revert_with_message(consts::ERR_STATIC)); + } else if context.address != cross_contract_call::ADDRESS.raw() { + 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, attached_near) = match args { + CrossContractCallArgs::Eager(call) => { + let call_gas = call.total_gas(); + let attached_near = call.total_near(); + let promise = PromiseCreateArgs { + target_account_id, + method: consts::ROUTER_EXEC_NAME.into(), + args: call + .try_to_vec() + .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) + } + }; + 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, + data: promise + .try_to_vec() + .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, + }; + + Ok(PrecompileOutput { + logs: vec![promise_log], + cost, + ..Default::default() + }) + } +} + +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() { + assert_eq!( + cross_contract_call::ADDRESS, + 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-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-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 05642d45f..7aeb7aaa8 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]) { @@ -95,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-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-standalone-storage/Cargo.toml b/engine-standalone-storage/Cargo.toml index f6f70109c..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.8.2" } +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" diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 89c6d2f48..407526e2f 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,22 @@ 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::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 395ebfbeb..4d732ab50 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,11 @@ 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), + 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, @@ -234,6 +240,9 @@ 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>), + FactorySetWNearAddress(types::Address), Unknown, } @@ -265,6 +274,13 @@ 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::FactorySetWNearAddress(address) => { + Self::FactorySetWNearAddress(*address) + } TransactionKind::Unknown => Self::Unknown, } } @@ -309,6 +325,13 @@ 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::FactorySetWNearAddress(address) => { + Ok(Self::FactorySetWNearAddress(address)) + } BorshableTransactionKind::Unknown => Ok(Self::Unknown), } } 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-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/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/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-tests/src/tests/erc20_connector.rs b/engine-tests/src/tests/erc20_connector.rs index be83cac26..2e438b15c 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}; @@ -800,7 +800,7 @@ 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 @@ 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 { @@ -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/promise_results_precompile.rs b/engine-tests/src/tests/promise_results_precompile.rs index e6fd09e1f..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); - - 20 * (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/res/w_near.wasm b/engine-tests/src/tests/res/w_near.wasm new file mode 100755 index 000000000..4c45e3d87 Binary files /dev/null and b/engine-tests/src/tests/res/w_near.wasm differ diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs new file mode 100644 index 000000000..6f9bbad15 --- /dev/null +++ b/engine-tests/src/tests/xcc.rs @@ -0,0 +1,600 @@ +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, 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::{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(); + 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(); + 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 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, u64) { + let data = CrossContractCallArgs::Eager(p).try_to_vec().unwrap(); + let input_length = data.len(); + let (submit_result, 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(); + 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, + ) + }; + + 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(500), + }; + // Shorter input + let (x1, y1, evm1) = profile_for_promise(PromiseArgs::Create(promise.clone())); + // longer input + let (x2, y2, evm2) = 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; + + assert!( + test_utils::within_x_percent( + 5, + 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!( + test_utils::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!( + 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!( + 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 + ); +} + +#[test] +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 chain_id = AuroraRunner::default().chain_id; + 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); + + // 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()), + 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 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: xcc_args.try_to_vec().unwrap(), + }; + 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, + ); + result.assert_success(); + let submit_result: aurora_engine::parameters::SubmitResult = result.unwrap_borsh(); + if !submit_result.status.is_ok() { + panic!("Unexpected result {:?}", submit_result); + } + + 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. + 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 + ); +} + +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(); + + 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 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()); + + 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"); + test_utils::rust::compile(base_path); + fs::read(output_path).unwrap() +} 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-types/src/parameters.rs b/engine-types/src/parameters.rs index 82ea30dcd..0c78fb1b7 100644 --- a/engine-types/src/parameters.rs +++ b/engine-types/src/parameters.rs @@ -10,6 +10,22 @@ 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, + } + } + + 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] #[derive(Debug, BorshSerialize, BorshDeserialize, Clone, PartialEq, Eq)] pub struct PromiseCreateArgs { @@ -29,6 +45,7 @@ pub struct PromiseWithCallbackArgs { #[derive(Debug, BorshSerialize, BorshDeserialize, Clone)] pub enum PromiseAction { + CreateAccount, Transfer { amount: Yocto, }, @@ -64,3 +81,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/storage.rs b/engine-types/src/storage.rs index a3a3738e8..e1b1be281 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 { @@ -45,6 +46,7 @@ impl From for u8 { Generation => 0x7, Nep141Erc20Map => 0x8, Erc20Nep141Map => 0x9, + CrossContractCall => 0xa, } } } @@ -91,6 +93,7 @@ impl From for KeyPrefix { 0x7 => Self::Generation, 0x8 => Self::Nep141Erc20Map, 0x9 => Self::Erc20Nep141Map, + 0xa => Self::CrossContractCall, _ => unreachable!(), } } 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 { 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/engine/src/engine.rs b/engine/src/engine.rs index 1b9ad86c1..7172881fc 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -16,12 +16,13 @@ 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::{ 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; @@ -242,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] { @@ -272,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] { @@ -361,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, } } @@ -554,7 +571,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(&self.io, handler, logs, &self.current_account_id); self.apply(values, Vec::::new(), true); @@ -639,7 +656,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(&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. @@ -1356,10 +1373,16 @@ 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: &I, + handler: &mut P, + logs: T, + current_account_id: &AccountId, +) -> Vec where T: IntoIterator, P: PromiseHandler, + I: IO + Copy, { logs.into_iter() .filter_map(|log| { @@ -1384,6 +1407,25 @@ where // `topics` field. Some(log.into()) } + } else if log.address == cross_contract_call::ADDRESS.raw() { + 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 } else { Some(log.into()) } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 607d79e7f..96be012ff 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] @@ -282,6 +283,40 @@ mod contract { ); } + /// 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_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::new(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); + } + + /// 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 new file mode 100644 index 000000000..564dae3a0 --- /dev/null +++ b/engine/src/xcc.rs @@ -0,0 +1,282 @@ +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::{format, Cow, Vec, U256}; +use borsh::{BorshDeserialize, BorshSerialize}; + +pub const ERR_NO_ROUTER_CODE: &str = "ERR_MISSING_XCC_BYTECODE"; +pub const ERR_INVALID_ACCOUNT: &str = "ERR_INVALID_XCC_ACCOUNT"; +pub const ERR_ATTACHED_NEAR: &str = "ERR_ATTACHED_XCC_NEAR"; +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(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)] +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 { + pub address: Address, + pub version: CodeVersion, +} + +pub fn handle_precompile_promise( + io: &I, + handler: &mut P, + promise: PromiseCreateArgs, + required_near: Yocto, + current_account_id: &AccountId, +) where + P: PromiseHandler, + 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); + + // 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 + ); + // 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); + 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, + }; + // 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 { + promise_actions.push(PromiseAction::CreateAccount); + promise_actions.push(PromiseAction::Transfer { + amount: STORAGE_AMOUNT, + }); + } + promise_actions.push(PromiseAction::DeployConotract { + 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: init_args.into_bytes(), + attached_yocto: ZERO_YOCTO, + gas: INITIALIZE_GAS, + }); + let batch = PromiseBatchAction { + 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, + 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, + }; + + Some(handler.promise_attach_callback(promise_id, &callback)) + } + 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), + }; +} + +/// 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::new(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()); +} + +/// 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. +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 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/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 new file mode 100644 index 000000000..615e1bbef --- /dev/null +++ b/etc/xcc-router/Cargo.lock @@ -0,0 +1,1267 @@ +# 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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.7", + "once_cell", + "version_check", +] + +[[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" +dependencies = [ + "borsh", + "hex", + "primitive-types 0.11.1", +] + +[[package]] +name = "autocfg" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +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 = "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" +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 = "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" +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 = "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" +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 = "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" +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 = "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 = "fixed-hash" +version = "0.7.0" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "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.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hex" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +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 = "b8ecf0b8b31aa7f4e60f629f72213a2617ca4a5f45cd1ae9ed2cf7cecfebdbb7" +dependencies = [ + "arrayref", + "blake2", + "borsh", + "bs58", + "c2-chacha", + "curve25519-dalek", + "derive_more", + "ed25519-dalek", + "libc", + "near-account-id", + "once_cell", + "parity-secp256k1", + "primitive-types 0.10.1", + "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", + "near-crypto", + "near-primitives-core", + "near-rpc-error-macro", + "near-vm-errors", + "num-rational", + "once_cell", + "primitive-types 0.10.1", + "rand 0.7.3", + "reed-solomon-erasure", + "serde", + "serde_json", + "smart-default", + "strum", + "thiserror", +] + +[[package]] +name = "near-primitives-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb561feb392bb8c4f540256073446e6689af087bf6356e8dddcf75fc279f201f" +dependencies = [ + "base64 0.11.0", + "borsh", + "bs58", + "derive_more", + "near-account-id", + "num-rational", + "serde", + "sha2 0.10.2", + "strum", +] + +[[package]] +name = "near-rpc-error-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fdd7ea8d8f786878651c37691515d5053f827ae60894aa40c16882b78f77c9" +dependencies = [ + "quote", + "serde", + "syn", +] + +[[package]] +name = "near-rpc-error-macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e521842b6ae864dfe5391afbbe2df9e9d8427c26e9333b2e0b65cd42094f7607" +dependencies = [ + "near-rpc-error-core", + "serde", + "syn", +] + +[[package]] +name = "near-sdk" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda34e06e28fb9a09ac54efbdc49f0c9308780fc932aaa81c49c493fde974045" +dependencies = [ + "base64 0.13.0", + "borsh", + "bs58", + "near-crypto", + "near-primitives", + "near-primitives-core", + "near-sdk-macros", + "near-sys", + "near-vm-logic", + "serde", + "serde_json", + "wee_alloc", +] + +[[package]] +name = "near-sdk-macros" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72064fcc15a623a0d40a6c199ea5cbdc30a83cae4816889d46f218acf31bfba8" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "near-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e307313276eaeced2ca95740b5639e1f3125b7c97f0a1151809d105f1aa8c6d3" + +[[package]] +name = "near-vm-errors" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e02faf2bc1f6ef82b965cfe44389808fb5594f7aca4b596766117f4ce74df20" +dependencies = [ + "borsh", + "near-account-id", + "near-rpc-error-macro", + "serde", +] + +[[package]] +name = "near-vm-logic" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f024d90451cd3c24d7a0a5cabf3636b192a60eb8e3ff0456f6c18b91152c346d" +dependencies = [ + "base64 0.13.0", + "borsh", + "bs58", + "byteorder", + "near-account-id", + "near-crypto", + "near-primitives", + "near-primitives-core", + "near-vm-errors", + "ripemd", + "serde", + "sha2 0.10.2", + "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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +] + +[[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-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" +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 = "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 = "a415a013dd7c5d4221382329a5a3482566da675737494935cbbbcdec04662f9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "ripemd" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1facec54cb5e0dc08553501fa740091086d0259ad0067e0d4103448e4cb22ed3" +dependencies = [ + "digest 0.10.3", +] + +[[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 = "rustversion" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" + +[[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 = [ + "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 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "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 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6" +dependencies = [ + "proc-macro2", + "quote", + "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 = "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 = "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" +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 = "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" +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 new file mode 100644 index 000000000..ba56690af --- /dev/null +++ b/etc/xcc-router/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "xcc_router" +version = "1.0.0" +authors = ["Aurora "] +edition = "2021" + +[lib] +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 } +near-sdk = "4.0.0" diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs new file mode 100644 index 000000000..1e6308b98 --- /dev/null +++ b/etc/xcc-router/src/lib.rs @@ -0,0 +1,219 @@ +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::{U128, U64}; +use near_sdk::BorshStorageKey; +use near_sdk::{env, near_bindgen, AccountId, Gas, PanicOnDefault, Promise, PromiseIndex}; + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod tests; + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey { + Version, + Parent, + Nonce, + Map, +} + +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)] +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 + 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, + /// Account ID for the wNEAR contract. + wnear_account: AccountId, +} + +#[near_bindgen] +impl Router { + #[init(ignore_state)] + 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 + // 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(StorageKey::Parent, None); + match parent.get() { + None => { + parent.set(&caller); + } + Some(parent) => { + if caller != parent { + env::panic_str(ERR_ILLEGAL_CALLER); + } + } + } + + 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 + + version.set(&CURRENT_VERSION); + } + + let nonce = LazyOption::new(StorageKey::Nonce, None); + let scheduled_promises = LookupMap::new(StorageKey::Map); + Self { + parent, + version, + nonce, + scheduled_promises, + wnear_account, + } + } + + /// 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)); + + near_sdk::log!("Promise scheduled at nonce {}", nonce); + } + + /// 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_str("ERR_PROMISE_NOT_FOUND"), + }; + + 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 { + fn require_parent_caller(&self) { + let caller = env::predecessor_account_id(); + let parent = self + .parent + .get() + .unwrap_or_else(|| env::panic_str("ERR_CONTRACT_NOT_INITIALIZED")); + if caller != parent { + env::panic_str(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, + 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().into(), + ) + } + + fn base_promise_create(promise: PromiseCreateArgs) -> PromiseIndex { + env::promise_create( + 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().into(), + ) + } +} diff --git a/etc/xcc-router/src/tests.rs b/etc/xcc-router/src/tests.rs new file mode 100644 index 000000000..ab9ee2a04 --- /dev/null +++ b/etc/xcc-router/src/tests.rs @@ -0,0 +1,211 @@ +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::testing_env; + +const WNEAR_ACCOUNT: &str = "wrap.near"; + +#[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(WNEAR_ACCOUNT.parse().unwrap(), false); + 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()) + .build()); + let _contract = Router::initialize(WNEAR_ACCOUNT.parse().unwrap(), false); +} + +#[test] +#[should_panic] +fn test_execute_wrong_caller() { + let (_parent, contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().as_str().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()) + .build()); + contract.execute(PromiseArgs::Create(promise)); +} + +#[test] +fn test_execute() { + let (_parent, contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().as_str().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 = test_utils::get_created_receipts(); + assert_eq!(receipts.len(), 1); + let receipt = receipts.pop().unwrap(); + assert_eq!( + receipt.receiver_id.as_str(), + 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().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().as_str().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 = 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); +} + +#[test] +#[should_panic] +fn test_schedule_wrong_caller() { + let (_parent, mut contract) = create_contract(); + + let promise = PromiseCreateArgs { + target_account_id: bob().as_str().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()) + .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().as_str().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 = test_utils::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()) + .build()); + contract.execute_scheduled(0.into()); + + assert_eq!(contract.nonce.get().unwrap(), 1); + assert!(!contract.scheduled_promises.contains_key(&0)); + + let mut receipts = test_utils::get_created_receipts(); + assert_eq!(receipts.len(), 1); + let receipt = receipts.pop().unwrap(); + 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: &[VmAction], promise: PromiseCreateArgs) { + assert_eq!(actions.len(), 1); + let action = &actions[0]; + + assert_eq!( + *action, + VmAction::FunctionCall { + function_name: promise.method, + args: promise.args, + gas: promise.attached_gas.as_u64().into(), + deposit: promise.attached_balance.as_u128() + } + ); +} + +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.clone()) + .build()); + let contract = Router::initialize(WNEAR_ACCOUNT.parse().unwrap(), false); + + (parent, contract) +}