diff --git a/CHANGES.md b/CHANGES.md index 48935660a..1ea294d19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,12 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.4.0] 2023-11-28 + +### Additions + +- Added a possibility to pass initialize arguments in json format to the `new` transaction by [@aleksuss]. ([#871]) +- The `SubmitResult` was made available for `ft_on_transfer` transactions in the standalone engine by [@birchmd]. ([#869]) +- The order of producing the exit precompile and XCC promises has been changed to sequential by [@birchmd]. ([#868]) + +### Changes + +- Removed the code hidden behind the feature that isn't used anymore by [@joshuajbouw]. ([#870]) +- The logic of unwrapping wNEAR has been changed to the Bridge's native by [@birchmd]. ([#867]) +- Bumped the `near-workspaces` to 0.9 by [@aleksuss]. ([#862]) + +### Fixes + +- Add a method for upgrading XCC router contract by [@birchmd]. ([#866]) +- Fixed a potential panic in the `ExitToNear` precompile by [@guidovranken]. ([#865]) +- Fixed a behaviour when the `ft_transfer` could occur before the `near_withdraw` by [@birchmd]. ([#864]) +- Fixed correctness of reproducing the NEAR runtime random value in the standalone engine by [@birchmd]. ([#863]) + +[#862]: https://github.com/aurora-is-near/aurora-engine/pull/862 +[#863]: https://github.com/aurora-is-near/aurora-engine/pull/863 +[#864]: https://github.com/aurora-is-near/aurora-engine/pull/864 +[#865]: https://github.com/aurora-is-near/aurora-engine/pull/865 +[#866]: https://github.com/aurora-is-near/aurora-engine/pull/866 +[#867]: https://github.com/aurora-is-near/aurora-engine/pull/867 +[#868]: https://github.com/aurora-is-near/aurora-engine/pull/868 +[#869]: https://github.com/aurora-is-near/aurora-engine/pull/869 +[#870]: https://github.com/aurora-is-near/aurora-engine/pull/870 +[#871]: https://github.com/aurora-is-near/aurora-engine/pull/871 + ## [3.3.1] 2023-10-26 ### Fixes - The smart contract owner whose account id is not the same as the contract account id can set ERC-20 metadata - by [@aleksuss] ([#858]). + by [@aleksuss]. ([#858]) [#858]: https://github.com/aurora-is-near/aurora-engine/pull/858 @@ -33,18 +65,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes - Changed structure `SetEthConnectorContractAccountArgs` for setting eth connector account. It was extended with - additional field: `withdraw_serialize_type` for defining serialization type for withdraw arguments by [@aleksuss]. ([#834]) - + additional field: `withdraw_serialize_type` for defining serialization type for withdraw arguments by [@aleksuss]. ([#834]) - Updated rocksdb up to 0.21.0 by [@aleksuss]. ([#840]) ### Additions - Added a possibility of mirroring deployed ERC-20 contracts in the main Aurora contract in Silo mode by [@aleksuss]. ([#845]) - - Allow to initialize hashchain directly with the `new` method by [@birchmd]. ([#846]) - - Added a silo logic which allows to set fixed gas costs per transaction by [@aleksuss]. ([#746]) - - Added a new type of transaction which allows to add full access key into account of the smart contract by [@aleksuss]. ([#847]) [#746]: https://github.com/aurora-is-near/aurora-engine/pull/746 @@ -59,9 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Additions - Added the possibility to use native NEAR instead of wNEAR on Aurora by [@karim-en]. ([#750]) - - Added hashchain integration by [@birchmd]. ([#831]) - - Added functions for setting and getting metadata of ERC-20 contracts deployed with `deploy_erc20_token` transaction by [@aleksuss]. ([#837]) @@ -547,7 +573,8 @@ struct SubmitResult { ## [1.0.0] - 2021-05-12 -[Unreleased]: https://github.com/aurora-is-near/aurora-engine/compare/3.3.1...develop +[Unreleased]: https://github.com/aurora-is-near/aurora-engine/compare/3.4.0...develop +[3.4.0]: https://github.com/aurora-is-near/aurora-engine/compare/3.3.1...3.4.0 [3.3.1]: https://github.com/aurora-is-near/aurora-engine/compare/3.3.0...3.3.1 [3.3.0]: https://github.com/aurora-is-near/aurora-engine/compare/3.2.0...3.3.0 [3.2.0]: https://github.com/aurora-is-near/aurora-engine/compare/3.1.0...3.2.0 diff --git a/Cargo.lock b/Cargo.lock index eaaf3fa4f..8175d8d4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ dependencies = [ [[package]] name = "aurora-engine" -version = "3.3.1" +version = "3.4.0" dependencies = [ "aurora-engine-hashchain", "aurora-engine-modexp", @@ -420,7 +420,6 @@ dependencies = [ "hex", "near-gas", "near-sdk", - "near-units", "near-workspaces", "rlp", "serde", @@ -466,7 +465,6 @@ dependencies = [ "lazy_static", "near-gas", "near-sdk", - "near-units", "near-workspaces", "serde", "serde_json", @@ -3434,35 +3432,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e307313276eaeced2ca95740b5639e1f3125b7c97f0a1151809d105f1aa8c6d3" [[package]] -name = "near-units" +name = "near-token" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a2b77f295d398589eeee51ad0887905ef1734fb12b45cb6d77bd7e401988b9" +checksum = "7b68f3f8a2409f72b43efdbeff8e820b81e70824c49fee8572979d789d1683fb" dependencies = [ - "near-units-core", - "near-units-macro", -] - -[[package]] -name = "near-units-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89aa2a7985de87a08ca35f28abd8d00f0f901e704257e6e029aadef981386bc6" -dependencies = [ - "num-format", - "regex", -] - -[[package]] -name = "near-units-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ab45d066220846f9bd5c21e9ab88c47c892edd36f962ada78bf8308523171a" -dependencies = [ - "near-units-core", - "proc-macro2", - "quote", - "syn 1.0.109", + "serde", ] [[package]] @@ -3571,9 +3546,9 @@ dependencies = [ [[package]] name = "near-workspaces" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f43c3cac1cf61d0f20fbc49f2c3182caa6422c0d2acd92c926a3e3190b26a9" +checksum = "3a14e772e49ba9644c06dad20f635b6463f74d378fa19822bfc35fef479c72e5" dependencies = [ "async-trait", "base64 0.21.4", @@ -3592,6 +3567,7 @@ dependencies = [ "near-primitives 0.17.0", "near-sandbox-utils", "near-sdk", + "near-token", "rand 0.8.5", "reqwest", "serde", @@ -3689,16 +3665,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec 0.7.4", - "itoa", -] - [[package]] name = "num-integer" version = "0.1.45" diff --git a/Cargo.toml b/Cargo.toml index 1200ae92d..e975287a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,8 +52,7 @@ near-sdk = "4" near-vm-errors = "0.17" near-vm-logic = "0.17" near-vm-runner = { version = "0.17", default-features = false, features = [ "wasmer2_vm", "wasmtime_vm" ] } -near-units = "0.2" -near-workspaces = "0.8" +near-workspaces = "0.9" num = { version = "0.4", default-features = false, features = ["alloc"] } postgres = "0.19" primitive-types = { version = "0.12", default-features = false, features = ["rlp", "serde_no_std"] } @@ -129,3 +128,11 @@ rpath = false # it to actually happen when running tests with --release lto = true opt-level = 3 + +# The profile is needed for faster linking in case we need to run locally a small amount of tests or just test +# business logic rather than test gas cost. E.g. of using the profile with the cargo: +# `cargo test --profile fast-link --features mainnet-test name_of_test_we_want_execute` +[profile.fast-link] +inherits = "dev" +opt-level = 0 +lto = false diff --git a/VERSION b/VERSION index bea438e9a..18091983f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.1 +3.4.0 diff --git a/engine-precompiles/src/native.rs b/engine-precompiles/src/native.rs index dc3a677dd..8c8aca8e4 100644 --- a/engine-precompiles/src/native.rs +++ b/engine-precompiles/src/native.rs @@ -349,7 +349,7 @@ impl Precompile for ExitToNear { // First byte of the input is a flag, selecting the behavior to be triggered: // 0x0 -> Eth transfer // 0x1 -> Erc20 transfer - let flag = input[0]; + let flag = input.first().copied().unwrap_or_default(); #[cfg(feature = "error_refund")] let (refund_address, mut input) = parse_input(input)?; #[cfg(not(feature = "error_refund"))] diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 874942438..8bfa7e660 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -52,6 +52,7 @@ mod consts { 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 ERR_XCC_ACCOUNT_ID: &str = "ERR_FAILED_TO_CREATE_XCC_ACCOUNT_ID"; pub(super) const ROUTER_EXEC_NAME: &str = "execute"; pub(super) const ROUTER_SCHEDULE_NAME: &str = "schedule"; /// Solidity selector for the ERC-20 transferFrom function @@ -130,7 +131,7 @@ impl HandleBasedPrecompile for CrossContractCall { } let sender = context.caller; - let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref()); + 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 { @@ -295,10 +296,13 @@ fn transfer_from_args(from: H160, to: H160, amount: U256) -> Vec { [&consts::TRANSFER_FROM_SELECTOR, args.as_slice()].concat() } -fn create_target_account_id(sender: H160, engine_account_id: &str) -> AccountId { +fn create_target_account_id( + sender: H160, + engine_account_id: &str, +) -> Result { format!("{}.{}", hex::encode(sender.as_bytes()), engine_account_id) .parse() - .unwrap_or_default() + .map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID)) } fn revert_with_message(message: &str) -> PrecompileFailure { diff --git a/engine-sdk/src/near_runtime.rs b/engine-sdk/src/near_runtime.rs index b9a9f3f38..be75fc24d 100644 --- a/engine-sdk/src/near_runtime.rs +++ b/engine-sdk/src/near_runtime.rs @@ -100,6 +100,118 @@ impl Runtime { ); } } + + #[allow(clippy::too_many_lines)] + unsafe fn append_batch_actions(id: u64, args: &PromiseBatchAction) { + for action in &args.actions { + match action { + PromiseAction::CreateAccount => { + exports::promise_batch_action_create_account(id); + } + PromiseAction::Transfer { amount } => { + let amount = amount.as_u128(); + let amount_addr = core::ptr::addr_of!(amount); + exports::promise_batch_action_transfer(id, amount_addr as _); + } + PromiseAction::DeployContract { code } => { + let code = code.as_slice(); + exports::promise_batch_action_deploy_contract( + id, + code.len() as _, + code.as_ptr() as _, + ); + } + PromiseAction::FunctionCall { + name, + gas, + attached_yocto, + args, + } => { + let method_name = name.as_bytes(); + let arguments = args.as_slice(); + let amount = attached_yocto.as_u128(); + let amount_addr = core::ptr::addr_of!(amount); + exports::promise_batch_action_function_call( + id, + method_name.len() as _, + method_name.as_ptr() as _, + arguments.len() as _, + arguments.as_ptr() as _, + amount_addr as _, + gas.as_u64(), + ); + } + PromiseAction::Stake { amount, public_key } => { + feature_gated!("all-promise-actions", { + let amount = amount.as_u128(); + let amount_addr = core::ptr::addr_of!(amount); + let pk: RawPublicKey = public_key.into(); + let pk_bytes = pk.as_bytes(); + exports::promise_batch_action_stake( + id, + amount_addr as _, + pk_bytes.len() as _, + pk_bytes.as_ptr() as _, + ); + }); + } + PromiseAction::AddFullAccessKey { public_key, nonce } => { + let pk: RawPublicKey = public_key.into(); + let pk_bytes = pk.as_bytes(); + exports::promise_batch_action_add_key_with_full_access( + id, + pk_bytes.len() as _, + pk_bytes.as_ptr() as _, + *nonce, + ); + } + PromiseAction::AddFunctionCallKey { + public_key, + nonce, + allowance, + receiver_id, + function_names, + } => { + let pk: RawPublicKey = public_key.into(); + let pk_bytes = pk.as_bytes(); + let allowance = allowance.as_u128(); + let allowance_addr = core::ptr::addr_of!(allowance); + let receiver_id = receiver_id.as_bytes(); + let function_names = function_names.as_bytes(); + exports::promise_batch_action_add_key_with_function_call( + id, + pk_bytes.len() as _, + pk_bytes.as_ptr() as _, + *nonce, + allowance_addr as _, + receiver_id.len() as _, + receiver_id.as_ptr() as _, + function_names.len() as _, + function_names.as_ptr() as _, + ); + } + PromiseAction::DeleteKey { public_key } => { + let pk: RawPublicKey = public_key.into(); + let pk_bytes = pk.as_bytes(); + exports::promise_batch_action_delete_key( + id, + pk_bytes.len() as _, + pk_bytes.as_ptr() as _, + ); + } + PromiseAction::DeleteAccount { beneficiary_id } => { + feature_gated!("all-promise-actions", { + let beneficiary_id = beneficiary_id.as_bytes(); + exports::promise_batch_action_delete_key( + id, + beneficiary_id.len() as _, + beneficiary_id.as_ptr() as _, + ); + }); + } + } + } + } } impl StorageIntermediate for RegisterIndex { @@ -350,120 +462,28 @@ impl crate::promise::PromiseHandler for Runtime { PromiseId::new(id) } - #[allow(clippy::too_many_lines)] unsafe fn promise_create_batch(&mut self, args: &PromiseBatchAction) -> PromiseId { let account_id = args.target_account_id.as_bytes(); let id = { exports::promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) }; - for action in &args.actions { - match action { - PromiseAction::CreateAccount => { - exports::promise_batch_action_create_account(id); - } - PromiseAction::Transfer { amount } => { - let amount = amount.as_u128(); - let amount_addr = core::ptr::addr_of!(amount); - exports::promise_batch_action_transfer(id, amount_addr as _); - } - PromiseAction::DeployContract { code } => { - let code = code.as_slice(); - exports::promise_batch_action_deploy_contract( - id, - code.len() as _, - code.as_ptr() as _, - ); - } - PromiseAction::FunctionCall { - name, - gas, - attached_yocto, - args, - } => { - let method_name = name.as_bytes(); - let arguments = args.as_slice(); - let amount = attached_yocto.as_u128(); - let amount_addr = core::ptr::addr_of!(amount); - exports::promise_batch_action_function_call( - id, - method_name.len() as _, - method_name.as_ptr() as _, - arguments.len() as _, - arguments.as_ptr() as _, - amount_addr as _, - gas.as_u64(), - ); - } - PromiseAction::Stake { amount, public_key } => { - feature_gated!("all-promise-actions", { - let amount = amount.as_u128(); - let amount_addr = core::ptr::addr_of!(amount); - let pk: RawPublicKey = public_key.into(); - let pk_bytes = pk.as_bytes(); - exports::promise_batch_action_stake( - id, - amount_addr as _, - pk_bytes.len() as _, - pk_bytes.as_ptr() as _, - ); - }); - } - PromiseAction::AddFullAccessKey { public_key, nonce } => { - let pk: RawPublicKey = public_key.into(); - let pk_bytes = pk.as_bytes(); - exports::promise_batch_action_add_key_with_full_access( - id, - pk_bytes.len() as _, - pk_bytes.as_ptr() as _, - *nonce, - ); - } - PromiseAction::AddFunctionCallKey { - public_key, - nonce, - allowance, - receiver_id, - function_names, - } => { - let pk: RawPublicKey = public_key.into(); - let pk_bytes = pk.as_bytes(); - let allowance = allowance.as_u128(); - let allowance_addr = core::ptr::addr_of!(allowance); - let receiver_id = receiver_id.as_bytes(); - let function_names = function_names.as_bytes(); - exports::promise_batch_action_add_key_with_function_call( - id, - pk_bytes.len() as _, - pk_bytes.as_ptr() as _, - *nonce, - allowance_addr as _, - receiver_id.len() as _, - receiver_id.as_ptr() as _, - function_names.len() as _, - function_names.as_ptr() as _, - ); - } - PromiseAction::DeleteKey { public_key } => { - let pk: RawPublicKey = public_key.into(); - let pk_bytes = pk.as_bytes(); - exports::promise_batch_action_delete_key( - id, - pk_bytes.len() as _, - pk_bytes.as_ptr() as _, - ); - } - PromiseAction::DeleteAccount { beneficiary_id } => { - feature_gated!("all-promise-actions", { - let beneficiary_id = beneficiary_id.as_bytes(); - exports::promise_batch_action_delete_key( - id, - beneficiary_id.len() as _, - beneficiary_id.as_ptr() as _, - ); - }); - } - } - } + Self::append_batch_actions(id, args); + + PromiseId::new(id) + } + + unsafe fn promise_attach_batch_callback( + &mut self, + base: PromiseId, + args: &PromiseBatchAction, + ) -> PromiseId { + let account_id = args.target_account_id.as_bytes(); + + let id = { + exports::promise_batch_then(base.raw(), account_id.len() as _, account_id.as_ptr() as _) + }; + + Self::append_batch_actions(id, args); PromiseId::new(id) } @@ -658,7 +678,11 @@ pub(crate) mod exports { ) -> u64; pub(crate) fn promise_and(promise_idx_ptr: u64, promise_idx_count: u64) -> u64; pub(crate) fn promise_batch_create(account_id_len: u64, account_id_ptr: u64) -> u64; - fn promise_batch_then(promise_index: u64, account_id_len: u64, account_id_ptr: u64) -> u64; + pub(crate) fn promise_batch_then( + promise_index: u64, + account_id_len: u64, + account_id_ptr: u64, + ) -> u64; // ####################### // # Promise API actions # // ####################### diff --git a/engine-sdk/src/promise.rs b/engine-sdk/src/promise.rs index 67a516348..8658d2605 100644 --- a/engine-sdk/src/promise.rs +++ b/engine-sdk/src/promise.rs @@ -51,6 +51,16 @@ pub trait PromiseHandler { /// code or adding/removing access keys. unsafe fn promise_create_batch(&mut self, args: &PromiseBatchAction) -> PromiseId; + /// # Safety + /// See note on `promise_create_call`. Promise batches in particular must be used very + /// carefully because they can take destructive actions such as deploying new contract + /// code or adding/removing access keys. + unsafe fn promise_attach_batch_callback( + &mut self, + base: PromiseId, + args: &PromiseBatchAction, + ) -> PromiseId; + fn promise_return(&mut self, promise: PromiseId); /// # Safety @@ -132,6 +142,14 @@ impl PromiseHandler for Noop { PromiseId::new(0) } + unsafe fn promise_attach_batch_callback( + &mut self, + _base: PromiseId, + _args: &PromiseBatchAction, + ) -> PromiseId { + PromiseId::new(0) + } + fn promise_return(&mut self, _promise: PromiseId) {} fn read_only(&self) -> Self::ReadOnly { diff --git a/engine-standalone-storage/src/promise.rs b/engine-standalone-storage/src/promise.rs index 35478668e..c76c512d6 100644 --- a/engine-standalone-storage/src/promise.rs +++ b/engine-standalone-storage/src/promise.rs @@ -53,6 +53,14 @@ impl<'a> PromiseHandler for NoScheduler<'a> { PromiseId::new(0) } + unsafe fn promise_attach_batch_callback( + &mut self, + _base: PromiseId, + _args: &PromiseBatchAction, + ) -> PromiseId { + PromiseId::new(0) + } + fn promise_return(&mut self, _promise: PromiseId) {} fn read_only(&self) -> Self::ReadOnly { diff --git a/engine-standalone-storage/src/relayer_db/mod.rs b/engine-standalone-storage/src/relayer_db/mod.rs index d36355438..b19434c5b 100644 --- a/engine-standalone-storage/src/relayer_db/mod.rs +++ b/engine-standalone-storage/src/relayer_db/mod.rs @@ -155,6 +155,7 @@ where transaction: crate::sync::types::TransactionKind::Submit(tx), promise_data: Vec::new(), raw_input: transaction_bytes, + action_hash: H256::default(), }; storage.set_transaction_included(tx_hash, &tx_msg, &diff)?; } @@ -268,6 +269,7 @@ mod test { transaction: TransactionKind::Unknown, promise_data: Vec::new(), raw_input: Vec::new(), + action_hash: H256::default(), }, &diff, ) diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 803c75fe0..faf73f20e 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -189,6 +189,10 @@ pub fn parse_transaction_kind( })?; TransactionKind::FactorySetWNearAddress(address) } + TransactionKindTag::WithdrawWnearToRouter => { + let args = xcc::WithdrawWnearToRouterArgs::try_from_slice(&bytes).map_err(f)?; + TransactionKind::WithdrawWnearToRouter(args) + } TransactionKindTag::SetUpgradeDelayBlocks => { let args = parameters::SetUpgradeDelayBlocksArgs::try_from_slice(&bytes).map_err(f)?; TransactionKind::SetUpgradeDelayBlocks(args) @@ -384,6 +388,10 @@ where let predecessor_account_id = transaction_message.caller.clone(); let near_receipt_id = transaction_message.near_receipt_id; let current_account_id = engine_account_id; + let random_seed = compute_random_seed( + &transaction_message.action_hash, + &block_metadata.random_seed, + ); let env = env::Fixed { signer_account_id, current_account_id, @@ -391,7 +399,7 @@ where block_height, block_timestamp: block_metadata.timestamp, attached_deposit: transaction_message.attached_near, - random_seed: block_metadata.random_seed, + random_seed, prepaid_gas: DEFAULT_PREPAID_GAS, }; @@ -435,6 +443,16 @@ where (tx_hash, diff, result) } +/// Based on nearcore implementation: +/// +fn compute_random_seed(action_hash: &H256, block_random_value: &H256) -> H256 { + const BYTES_LEN: usize = 32 + 32; + let mut bytes: Vec = Vec::with_capacity(BYTES_LEN); + bytes.extend_from_slice(action_hash.as_bytes()); + bytes.extend_from_slice(block_random_value.as_bytes()); + aurora_engine_sdk::sha256(&bytes) +} + /// Handles all transaction kinds other than `submit`. /// The `submit` transaction kind is special because it is the only one where the transaction hash /// differs from the NEAR receipt hash. @@ -475,9 +493,9 @@ fn non_submit_execute( TransactionKind::FtOnTransfer(_) => { // No promises can be created by `ft_on_transfer` let mut handler = crate::promise::NoScheduler { promise_data }; - contract_methods::connector::ft_on_transfer(io, env, &mut handler)?; + let maybe_output = contract_methods::connector::ft_on_transfer(io, env, &mut handler)?; - None + maybe_output.map(|result| TransactionExecutionResult::Submit(Ok(result))) } TransactionKind::FtTransferCall(_) => { #[cfg(feature = "ext-connector")] @@ -630,6 +648,12 @@ fn non_submit_execute( None } + TransactionKind::WithdrawWnearToRouter(_) => { + let mut handler = crate::promise::NoScheduler { promise_data }; + let result = contract_methods::xcc::withdraw_wnear_to_router(io, env, &mut handler)?; + + Some(TransactionExecutionResult::Submit(Ok(result))) + } TransactionKind::Unknown => None, // Not handled in this function; is handled by the general `execute_transaction` function TransactionKind::Submit(_) | TransactionKind::SubmitWithArgs(_) => unreachable!(), diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index 06ae54ade..c30a3d5e3 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -5,6 +5,7 @@ use aurora_engine::xcc::{AddressVersionUpdateArgs, FundXccArgs}; use aurora_engine_transactions::{EthTransactionKind, NormalizedEthTransaction}; use aurora_engine_types::account_id::AccountId; use aurora_engine_types::parameters::silo; +use aurora_engine_types::parameters::xcc::WithdrawWnearToRouterArgs; use aurora_engine_types::types::Address; use aurora_engine_types::{ borsh::{self, BorshDeserialize, BorshSerialize}, @@ -53,6 +54,14 @@ pub struct TransactionMessage { pub promise_data: Vec>>, /// Raw bytes passed as input when executed in the Near Runtime. pub raw_input: Vec, + /// A Near protocol quantity equal to + /// `sha256(receipt_id || block_hash || le_bytes(u64 - action_index))`. + /// This quantity is used together with the block random seed + /// to generate the random value available to the transaction. + /// nearcore references: + /// - https://github.com/near/nearcore/blob/00ca2f3f73e2a547ba881f76ecc59450dbbef6e2/core/primitives/src/utils.rs#L261 + /// - https://github.com/near/nearcore/blob/00ca2f3f73e2a547ba881f76ecc59450dbbef6e2/core/primitives/src/utils.rs#L295 + pub action_hash: H256, } impl TransactionMessage { @@ -138,6 +147,8 @@ pub enum TransactionKind { FactoryUpdateAddressVersion(AddressVersionUpdateArgs), FactorySetWNearAddress(Address), FundXccSubAccount(FundXccArgs), + /// Self-call used during XCC flow to move wNEAR tokens to user's XCC account + WithdrawWnearToRouter(WithdrawWnearToRouterArgs), /// Pause the contract PauseContract, /// Resume the contract @@ -366,6 +377,31 @@ impl TransactionKind { }, ) } + Self::WithdrawWnearToRouter(args) => { + let recipient = AccountId::new(&format!( + "{}.{}", + args.target.encode(), + engine_account.as_ref() + )) + .unwrap_or_else(|_| engine_account.clone()); + let wnear_address = storage + .with_engine_access(block_height, transaction_position, &[], |io| { + aurora_engine_precompiles::xcc::state::get_wnear_address(&io) + }) + .result; + let call_args = aurora_engine::xcc::withdraw_wnear_call_args( + &recipient, + args.amount, + wnear_address, + ); + Self::Call(call_args).eth_repr( + engine_account, + caller, + block_height, + transaction_position, + storage, + ) + } Self::Deposit(_) => Self::no_evm_execution("deposit"), Self::FtTransferCall(_) => Self::no_evm_execution("ft_transfer_call"), Self::FinishDeposit(_) => Self::no_evm_execution("finish_deposit"), @@ -542,6 +578,8 @@ pub enum TransactionKindTag { RemoveEntryFromWhitelist, #[strum(serialize = "mirror_erc20_token_callback")] MirrorErc20TokenCallback, + #[strum(serialize = "withdraw_wnear_to_router")] + WithdrawWnearToRouter, Unknown, } @@ -584,6 +622,7 @@ impl TransactionKind { Self::NewEngine(args) => args.try_to_vec().unwrap_or_default(), Self::FactoryUpdateAddressVersion(args) => args.try_to_vec().unwrap_or_default(), Self::FundXccSubAccount(args) => args.try_to_vec().unwrap_or_default(), + Self::WithdrawWnearToRouter(args) => args.try_to_vec().unwrap_or_default(), Self::PauseContract | Self::ResumeContract | Self::Unknown => Vec::new(), Self::SetKeyManager(args) => args.try_to_vec().unwrap_or_default(), Self::AddRelayerKey(args) | Self::RemoveRelayerKey(args) => { @@ -633,6 +672,7 @@ impl From<&TransactionKind> for TransactionKindTag { TransactionKind::FactoryUpdate(_) => Self::FactoryUpdate, TransactionKind::FactoryUpdateAddressVersion(_) => Self::FactoryUpdateAddressVersion, TransactionKind::FactorySetWNearAddress(_) => Self::FactorySetWNearAddress, + TransactionKind::WithdrawWnearToRouter(_) => Self::WithdrawWnearToRouter, TransactionKind::SetOwner(_) => Self::SetOwner, TransactionKind::SubmitWithArgs(_) => Self::SubmitWithArgs, TransactionKind::SetUpgradeDelayBlocks(_) => Self::SetUpgradeDelayBlocks, @@ -679,6 +719,7 @@ enum BorshableTransactionMessage<'a> { V1(BorshableTransactionMessageV1<'a>), V2(BorshableTransactionMessageV2<'a>), V3(BorshableTransactionMessageV3<'a>), + V4(BorshableTransactionMessageV4<'a>), } #[derive(BorshDeserialize, BorshSerialize)] @@ -720,6 +761,21 @@ struct BorshableTransactionMessageV3<'a> { pub raw_input: Cow<'a, Vec>, } +#[derive(BorshDeserialize, BorshSerialize)] +struct BorshableTransactionMessageV4<'a> { + pub block_hash: [u8; 32], + pub near_receipt_id: [u8; 32], + pub position: u16, + pub succeeded: bool, + pub signer: Cow<'a, AccountId>, + pub caller: Cow<'a, AccountId>, + pub attached_near: u128, + pub transaction: BorshableTransactionKind<'a>, + pub promise_data: Cow<'a, Vec>>>, + pub raw_input: Cow<'a, Vec>, + pub action_hash: [u8; 32], +} + impl<'a> From<&'a TransactionMessage> for BorshableTransactionMessage<'a> { fn from(t: &'a TransactionMessage) -> Self { Self::V3(BorshableTransactionMessageV3 { @@ -756,6 +812,7 @@ impl<'a> TryFrom> for TransactionMessage { transaction, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }) } BorshableTransactionMessage::V2(t) => { @@ -772,6 +829,7 @@ impl<'a> TryFrom> for TransactionMessage { transaction, promise_data: t.promise_data.into_owned(), raw_input, + action_hash: H256::default(), }) } BorshableTransactionMessage::V3(t) => Ok(Self { @@ -785,6 +843,20 @@ impl<'a> TryFrom> for TransactionMessage { transaction: t.transaction.try_into()?, promise_data: t.promise_data.into_owned(), raw_input: t.raw_input.into_owned(), + action_hash: H256::default(), + }), + BorshableTransactionMessage::V4(t) => Ok(Self { + block_hash: H256(t.block_hash), + near_receipt_id: H256(t.near_receipt_id), + position: t.position, + succeeded: t.succeeded, + signer: t.signer.into_owned(), + caller: t.caller.into_owned(), + attached_near: t.attached_near, + transaction: t.transaction.try_into()?, + promise_data: t.promise_data.into_owned(), + raw_input: t.raw_input.into_owned(), + action_hash: H256(t.action_hash), }), } } @@ -846,6 +918,7 @@ enum BorshableTransactionKind<'a> { SetWhitelistStatus(Cow<'a, silo::WhitelistStatusArgs>), SetEthConnectorContractAccount(Cow<'a, parameters::SetEthConnectorContractAccountArgs>), MirrorErc20TokenCallback(Cow<'a, parameters::MirrorErc20TokenArgs>), + WithdrawWnearToRouter(Cow<'a, WithdrawWnearToRouterArgs>), } impl<'a> From<&'a TransactionKind> for BorshableTransactionKind<'a> { @@ -884,6 +957,9 @@ impl<'a> From<&'a TransactionKind> for BorshableTransactionKind<'a> { TransactionKind::FactorySetWNearAddress(address) => { Self::FactorySetWNearAddress(*address) } + TransactionKind::WithdrawWnearToRouter(x) => { + Self::WithdrawWnearToRouter(Cow::Borrowed(x)) + } TransactionKind::Unknown => Self::Unknown, TransactionKind::PausePrecompiles(x) => Self::PausePrecompiles(Cow::Borrowed(x)), TransactionKind::ResumePrecompiles(x) => Self::ResumePrecompiles(Cow::Borrowed(x)), @@ -1011,6 +1087,9 @@ impl<'a> TryFrom> for TransactionKind { BorshableTransactionKind::MirrorErc20TokenCallback(x) => { Ok(Self::MirrorErc20TokenCallback(x.into_owned())) } + BorshableTransactionKind::WithdrawWnearToRouter(x) => { + Ok(Self::WithdrawWnearToRouter(x.into_owned())) + } } } } diff --git a/engine-test-doubles/src/promise.rs b/engine-test-doubles/src/promise.rs index 7da0e1a26..cb4f47103 100644 --- a/engine-test-doubles/src/promise.rs +++ b/engine-test-doubles/src/promise.rs @@ -33,6 +33,22 @@ impl PromiseTracker { self.internal_index += 1; id } + + fn remove_as_near_promise(&mut self, id: u64) -> Option { + let result = match self.scheduled_promises.remove(&id)? { + PromiseArgs::Batch(x) => NearPromise::Simple(SimpleNearPromise::Batch(x)), + PromiseArgs::Create(x) => NearPromise::Simple(SimpleNearPromise::Create(x)), + PromiseArgs::Recursive(x) => x, + PromiseArgs::Callback { base, callback } => { + let base_promise = self.remove_as_near_promise(base.raw())?; + NearPromise::Then { + base: Box::new(base_promise), + callback: SimpleNearPromise::Create(callback), + } + } + }; + Some(result) + } } impl PromiseHandler for PromiseTracker { @@ -91,6 +107,23 @@ impl PromiseHandler for PromiseTracker { PromiseId::new(id) } + unsafe fn promise_attach_batch_callback( + &mut self, + base: PromiseId, + args: &PromiseBatchAction, + ) -> PromiseId { + let id = self.take_id(); + let base_promise = self + .remove_as_near_promise(base.raw()) + .expect("Base promise id must be known"); + let new_promise = PromiseArgs::Recursive(NearPromise::Then { + base: Box::new(base_promise), + callback: SimpleNearPromise::Batch(args.clone()), + }); + self.scheduled_promises.insert(id, new_promise); + PromiseId::new(id) + } + fn promise_return(&mut self, promise: PromiseId) { self.returned_promise = Some(promise); } diff --git a/engine-tests-connector/Cargo.toml b/engine-tests-connector/Cargo.toml index 4f30d3f02..3787669c8 100644 --- a/engine-tests-connector/Cargo.toml +++ b/engine-tests-connector/Cargo.toml @@ -18,7 +18,6 @@ anyhow.workspace = true byte-slice-cast.workspace = true near-gas.workspace = true near-sdk.workspace = true -near-units.workspace = true near-workspaces.workspace = true tokio = { workspace = true, features = ["macros"] } hex.workspace = true diff --git a/engine-tests-connector/src/connector.rs b/engine-tests-connector/src/connector.rs index 32fe6ca8d..7eb80cd9a 100644 --- a/engine-tests-connector/src/connector.rs +++ b/engine-tests-connector/src/connector.rs @@ -9,10 +9,13 @@ use aurora_engine_types::{ }; use byte_slice_cast::AsByteSlice; use near_sdk::serde_json::json; -use near_sdk::{json_types::U128, serde, ONE_YOCTO}; +use near_sdk::{json_types::U128, serde}; +use near_workspaces::types::NearToken; use near_workspaces::AccountId; use std::str::FromStr; +const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1); + /// Bytes for a NEAR smart contract implementing `ft_on_transfer` fn dummy_ft_receiver_bytes() -> Vec { let base_path = std::path::Path::new("../etc") diff --git a/engine-tests-connector/src/utils.rs b/engine-tests-connector/src/utils.rs index 6e3cee883..6cecebf3d 100644 --- a/engine-tests-connector/src/utils.rs +++ b/engine-tests-connector/src/utils.rs @@ -6,6 +6,7 @@ use aurora_engine_types::types::{Address, Wei}; use near_sdk::serde_json::json; use near_sdk::{json_types::U128, serde_json}; use near_workspaces::network::NetworkClient; +use near_workspaces::types::NearToken; use near_workspaces::{result::ExecutionFinalResult, Account, AccountId, Contract, Worker}; use std::path::Path; @@ -63,7 +64,7 @@ impl TestContract { .batch(&root) .create_account() .add_key(sk.public_key(), AccessKey::full_access()) - .transfer(near_units::parse_near!("100 N")) + .transfer(NearToken::from_near(100)) .transact() .await? .into_result()?; @@ -71,13 +72,13 @@ impl TestContract { let root_account = Account::from_secret_key(root, sk, &worker); let eth_connector = root_account .create_subaccount("aurora_eth_connector") - .initial_balance(near_units::parse_near!("15 N")) + .initial_balance(NearToken::from_near(15)) .transact() .await? .into_result()?; let engine = root_account .create_subaccount("eth_connector") - .initial_balance(near_units::parse_near!("15 N")) + .initial_balance(NearToken::from_near(15)) .transact() .await? .into_result()?; @@ -204,7 +205,7 @@ impl TestContract { Ok(self .root_account .create_subaccount(name) - .initial_balance(near_units::parse_near!("15 N")) + .initial_balance(NearToken::from_near(15)) .transact() .await? .into_result()?) diff --git a/engine-tests/src/tests/access_keys.rs b/engine-tests/src/tests/access_keys.rs index 2664bd3dc..1f7b04c74 100644 --- a/engine-tests/src/tests/access_keys.rs +++ b/engine-tests/src/tests/access_keys.rs @@ -4,11 +4,13 @@ use aurora_engine_types::parameters::engine::{ }; use aurora_engine_types::public_key::PublicKey; use aurora_engine_types::types::Address; -use aurora_engine_workspace::parse_near; -use aurora_engine_workspace::types::{KeyType, SecretKey}; +use aurora_engine_workspace::types::{KeyType, NearToken, SecretKey}; use std::fmt::Debug; use std::str::FromStr; +const BALANCE: NearToken = NearToken::from_near(10); +const DEPOSIT: NearToken = NearToken::from_millinear(500); + #[tokio::test] async fn test_add_key_manager() { let aurora = deploy_engine().await; @@ -16,14 +18,14 @@ async fn test_add_key_manager() { let relayer_key_args = RelayerKeyArgs { public_key: pk }; let manager = aurora .root() - .create_subaccount("key_manager", parse_near!("10 N")) + .create_subaccount("key_manager", BALANCE) .await .unwrap(); let result = manager .call(&aurora.id(), "add_relayer_key") .args_json(relayer_key_args.clone()) - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .max_gas() .transact() .await @@ -46,7 +48,7 @@ async fn test_add_key_manager() { .call(&aurora.id(), "add_relayer_key") .args_json(relayer_key_args.clone()) .max_gas() - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .transact() .await .unwrap(); @@ -64,7 +66,7 @@ async fn test_add_key_manager() { .call(&aurora.id(), "add_relayer_key") .args_json(relayer_key_args) .max_gas() - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .transact() .await .unwrap(); @@ -82,7 +84,7 @@ async fn test_submit_by_relayer() { let manager = aurora .root() - .create_subaccount("key_manager", parse_near!("10 N")) + .create_subaccount("key_manager", BALANCE) .await .unwrap(); let result = aurora @@ -108,7 +110,7 @@ async fn test_submit_by_relayer() { .call(&aurora.id(), "add_relayer_key") .args_json(RelayerKeyArgs { public_key }) .max_gas() - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .transact() .await .unwrap(); @@ -131,7 +133,7 @@ async fn test_delete_relayer_key() { let manager = aurora .root() - .create_subaccount("key_manager", parse_near!("10 N")) + .create_subaccount("key_manager", BALANCE) .await .unwrap(); let result = aurora @@ -148,7 +150,7 @@ async fn test_delete_relayer_key() { .call(&aurora.id(), "add_relayer_key") .args_json(RelayerKeyArgs { public_key }) .max_gas() - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .transact() .await .unwrap(); @@ -199,7 +201,7 @@ async fn test_call_not_allowed_method() { let manager = aurora .root() - .create_subaccount("key_manager", parse_near!("10 N")) + .create_subaccount("key_manager", BALANCE) .await .unwrap(); let result = aurora @@ -216,7 +218,7 @@ async fn test_call_not_allowed_method() { .call(&aurora.id(), "add_relayer_key") .args_json(RelayerKeyArgs { public_key }) .max_gas() - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .transact() .await .unwrap(); @@ -243,7 +245,7 @@ async fn test_call_not_allowed_contract() { let manager = aurora .root() - .create_subaccount("key_manager", parse_near!("10 N")) + .create_subaccount("key_manager", BALANCE) .await .unwrap(); let result = aurora @@ -260,7 +262,7 @@ async fn test_call_not_allowed_contract() { .call(&aurora.id(), "add_relayer_key") .args_json(RelayerKeyArgs { public_key }) .max_gas() - .deposit(parse_near!("0.5 N")) + .deposit(DEPOSIT) .transact() .await .unwrap(); diff --git a/engine-tests/src/tests/erc20_connector.rs b/engine-tests/src/tests/erc20_connector.rs index 497dfa616..67547f51a 100644 --- a/engine-tests/src/tests/erc20_connector.rs +++ b/engine-tests/src/tests/erc20_connector.rs @@ -398,9 +398,10 @@ pub mod workspace { use aurora_engine::proof::Proof; use aurora_engine_types::parameters::engine::TransactionStatus; use aurora_engine_workspace::account::Account; - use aurora_engine_workspace::types::ExecutionFinalResult; - use aurora_engine_workspace::{parse_near, EngineContract, RawContract}; + use aurora_engine_workspace::types::{ExecutionFinalResult, NearToken}; + use aurora_engine_workspace::{EngineContract, RawContract}; + const BALANCE: NearToken = NearToken::from_near(50); const FT_TOTAL_SUPPLY: u128 = 1_000_000; const FT_TRANSFER_AMOUNT: u128 = 300_000; const FT_EXIT_AMOUNT: u128 = 100_000; @@ -532,7 +533,11 @@ pub mod workspace { &aurora, ) .await; - let total_tokens_burnt: u128 = result.outcomes().iter().map(|o| o.tokens_burnt).sum(); + let total_tokens_burnt: u128 = result + .outcomes() + .iter() + .map(|o| o.tokens_burnt.as_yoctonear()) + .sum(); // Check that the wnear tokens are properly unwrapped and transferred to `ft_owner` assert_eq!( @@ -578,7 +583,11 @@ pub mod workspace { &aurora, ) .await; - let total_tokens_burnt: u128 = result.outcomes().iter().map(|o| o.tokens_burnt).sum(); + let total_tokens_burnt: u128 = result + .outcomes() + .iter() + .map(|o| o.tokens_burnt.as_yoctonear()) + .sum(); // Check that there were no near tokens transferred to `ft_owner` assert_eq!( @@ -717,7 +726,7 @@ pub mod workspace { None, ) .max_gas() - .deposit(1) + .deposit(NearToken::from_yoctonear(1)) .transact() .await .unwrap(); @@ -761,7 +770,6 @@ pub mod workspace { ); } - #[allow(clippy::future_not_send)] async fn test_exit_to_near_eth_common() -> anyhow::Result { let aurora = deploy_engine().await; let chain_id = aurora.get_chain_id().await?.result.as_u64(); @@ -808,13 +816,13 @@ pub mod workspace { }) } - #[allow(clippy::future_not_send, clippy::cognitive_complexity)] + #[allow(clippy::cognitive_complexity)] async fn test_exit_to_near_common() -> anyhow::Result { // 1. deploy Aurora let aurora = deploy_engine().await; // 2. Create account - let ft_owner = create_sub_account(&aurora.root(), "ft_owner", parse_near!("50 N")).await?; + let ft_owner = create_sub_account(&aurora.root(), "ft_owner", BALANCE).await?; let ft_owner_address = aurora_engine_sdk::types::near_account_to_evm_address(ft_owner.id().as_bytes()); let result = aurora @@ -866,8 +874,7 @@ pub mod workspace { ); // 5. Deploy NEP-141 - let nep_141_account = - create_sub_account(&aurora.root(), FT_ACCOUNT, parse_near!("50 N")).await?; + let nep_141_account = create_sub_account(&aurora.root(), FT_ACCOUNT, BALANCE).await?; let nep_141 = deploy_nep_141(&nep_141_account, &ft_owner, FT_TOTAL_SUPPLY, &aurora) .await diff --git a/engine-tests/src/tests/erc20_mirror.rs b/engine-tests/src/tests/erc20_mirror.rs index a189baa08..bf3a4c0b0 100644 --- a/engine-tests/src/tests/erc20_mirror.rs +++ b/engine-tests/src/tests/erc20_mirror.rs @@ -5,7 +5,6 @@ use crate::utils::workspace::{ transfer_nep_141_to_erc_20, }; use crate::utils::AuroraRunner; -use aurora_engine_precompiles::xcc::state::STORAGE_AMOUNT; use aurora_engine_types::parameters::connector::{ Erc20Identifier, Erc20Metadata, MirrorErc20TokenArgs, SetErc20MetadataArgs, WithdrawSerializeType, @@ -13,7 +12,8 @@ use aurora_engine_types::parameters::connector::{ use aurora_engine_types::parameters::silo::SiloParamsArgs; use aurora_engine_types::types::RawU256; use aurora_engine_workspace::account::Account; -use aurora_engine_workspace::{parse_near, EngineContract, RawContract}; +use aurora_engine_workspace::types::NearToken; +use aurora_engine_workspace::{EngineContract, RawContract}; const AURORA_VERSION: &str = include_str!("../../../VERSION"); const TRANSFER_AMOUNT: u128 = 1000; @@ -95,7 +95,7 @@ async fn test_mirroring_erc20_token() { .args_json(serde_json::json!({ "account_id": silo_contract.id(), })) - .deposit(STORAGE_AMOUNT.as_u128()) + .deposit(NearToken::from_near(2)) .transact() .await .unwrap(); @@ -169,7 +169,6 @@ async fn test_mirroring_erc20_token() { assert_eq!(nep_141_balance_of(&nep141, &ft_owner.id()).await, 1_000_000); } -#[allow(clippy::future_not_send)] async fn deploy_main_contract() -> EngineContract { let code = get_main_contract_code().await.unwrap(); deploy_engine_with_code(code).await @@ -178,7 +177,7 @@ async fn deploy_main_contract() -> EngineContract { async fn deploy_silo_contract(main_contract: &EngineContract) -> EngineContract { let silo_account = main_contract .root() - .create_subaccount("silo", parse_near!("50 N")) + .create_subaccount("silo", NearToken::from_near(50)) .await .unwrap(); let silo_bytes = AuroraRunner::get_engine_code(); @@ -211,12 +210,12 @@ async fn deploy_silo_contract(main_contract: &EngineContract) -> EngineContract async fn deploy_nep141(main_contract: &EngineContract) -> (RawContract, Account) { let ft_owner = main_contract .root() - .create_subaccount("ft_owner", parse_near!("10 N")) + .create_subaccount("ft_owner", NearToken::from_near(10)) .await .unwrap(); let nep_141_account = main_contract .root() - .create_subaccount("test_token", parse_near!("10 N")) + .create_subaccount("test_token", NearToken::from_near(10)) .await .unwrap(); diff --git a/engine-tests/src/tests/random.rs b/engine-tests/src/tests/random.rs index 473887ab1..35b4f9358 100644 --- a/engine-tests/src/tests/random.rs +++ b/engine-tests/src/tests/random.rs @@ -1,12 +1,17 @@ use crate::utils; use crate::utils::solidity::random::{Random, RandomConstructor}; use aurora_engine_types::H256; +use rand::SeedableRng; #[test] fn test_random_number_precompile() { let random_seed = H256::from_slice(vec![7; 32].as_slice()); - let mut signer = utils::Signer::random(); - let mut runner = utils::deploy_runner().with_random_seed(random_seed); + let secret_key = { + let mut rng = rand::rngs::StdRng::from_seed(random_seed.0); + libsecp256k1::SecretKey::random(&mut rng) + }; + let mut signer = utils::Signer::new(secret_key); + let mut runner = utils::deploy_runner().with_block_random_value(random_seed); let random_ctr = RandomConstructor::load(); let nonce = signer.use_nonce(); @@ -14,6 +19,13 @@ fn test_random_number_precompile() { .deploy_contract(&signer.secret_key, |ctr| ctr.deploy(nonce), random_ctr) .into(); + // Value derived from `random_seed` above together with the `action_hash` + // of the following transaction. + let expected_value = H256::from_slice( + &hex::decode("1a71249ace8312de8ed3640c852d5d542b04b2caec668325f6e18811244e7f5c").unwrap(), + ); + runner.context.random_seed = expected_value.0.to_vec(); + let counter_value = random.random_seed(&mut runner, &mut signer); - assert_eq!(counter_value, random_seed); + assert_eq!(counter_value, expected_value); } diff --git a/engine-tests/src/tests/repro.rs b/engine-tests/src/tests/repro.rs index 35b7eede4..2615e8c44 100644 --- a/engine-tests/src/tests/repro.rs +++ b/engine-tests/src/tests/repro.rs @@ -171,7 +171,7 @@ fn repro_common(context: &ReproContext) { let mut standalone = standalone::StandaloneRunner::default(); json_snapshot::initialize_engine_state(&standalone.storage, snapshot).unwrap(); let standalone_result = standalone - .submit_raw("submit", &runner.context, &[]) + .submit_raw("submit", &runner.context, &[], None) .unwrap(); assert_eq!( submit_result.try_to_vec().unwrap(), diff --git a/engine-tests/src/tests/res/aurora_silo_v3.3.1.wasm b/engine-tests/src/tests/res/aurora_silo_v3.3.1.wasm new file mode 100755 index 000000000..3a01a3586 Binary files /dev/null and b/engine-tests/src/tests/res/aurora_silo_v3.3.1.wasm differ diff --git a/engine-tests/src/tests/res/aurora_v3.3.1.wasm b/engine-tests/src/tests/res/aurora_v3.3.1.wasm new file mode 100755 index 000000000..7177a1117 Binary files /dev/null and b/engine-tests/src/tests/res/aurora_v3.3.1.wasm differ diff --git a/engine-tests/src/tests/res/xcc_router_v1.wasm b/engine-tests/src/tests/res/xcc_router_v1.wasm new file mode 100755 index 000000000..55561e7c9 Binary files /dev/null and b/engine-tests/src/tests/res/xcc_router_v1.wasm differ diff --git a/engine-tests/src/tests/sanity.rs b/engine-tests/src/tests/sanity.rs index 3dfa74211..462fb097d 100644 --- a/engine-tests/src/tests/sanity.rs +++ b/engine-tests/src/tests/sanity.rs @@ -212,14 +212,18 @@ fn test_transaction_to_zero_address() { // Prior to the fix the zero address is interpreted as None, causing a contract deployment. // It also incorrectly derives the sender address, so does not increment the right nonce. context.block_height = ZERO_ADDRESS_FIX_HEIGHT - 1; - let result = runner.submit_raw(utils::SUBMIT, &context, &[]).unwrap(); + let result = runner + .submit_raw(utils::SUBMIT, &context, &[], None) + .unwrap(); assert_eq!(result.gas_used, 53_000); runner.env.block_height = ZERO_ADDRESS_FIX_HEIGHT; assert_eq!(runner.get_nonce(&address), U256::zero()); // After the fix this transaction is simply a transfer of 0 ETH to the zero address context.block_height = ZERO_ADDRESS_FIX_HEIGHT; - let result = runner.submit_raw(utils::SUBMIT, &context, &[]).unwrap(); + let result = runner + .submit_raw(utils::SUBMIT, &context, &[], None) + .unwrap(); assert_eq!(result.gas_used, 21_000); runner.env.block_height = ZERO_ADDRESS_FIX_HEIGHT + 1; assert_eq!(runner.get_nonce(&address), U256::one()); @@ -685,7 +689,7 @@ fn test_num_wasm_functions() { let module = walrus::ModuleConfig::default() .parse(runner.code.code()) .unwrap(); - let expected_number = 1550; + let expected_number = 1600; let actual_number = module.funcs.iter().count(); assert!( @@ -1297,7 +1301,6 @@ mod workspace { ); } - #[allow(clippy::future_not_send)] async fn initialize_engine() -> (EngineContract, utils::Signer, Address) { let engine = utils::workspace::deploy_engine().await; let signer = utils::Signer::random(); diff --git a/engine-tests/src/tests/silo.rs b/engine-tests/src/tests/silo.rs index 5983583ad..613ac99d9 100644 --- a/engine-tests/src/tests/silo.rs +++ b/engine-tests/src/tests/silo.rs @@ -904,7 +904,8 @@ pub mod workspace { SiloParamsArgs, WhitelistAddressArgs, WhitelistArgs, WhitelistKind, }; use aurora_engine_types::types::Address; - use aurora_engine_workspace::{account::Account, parse_near, EngineContract, RawContract}; + use aurora_engine_workspace::types::NearToken; + use aurora_engine_workspace::{account::Account, EngineContract, RawContract}; const FT_ACCOUNT: &str = "test_token"; const FT_TOTAL_SUPPLY: u128 = 1_000_000; @@ -1090,7 +1091,7 @@ pub mod workspace { let result = source .call(&nep_141.id(), "ft_transfer_call") .args_json(transfer_args) - .deposit(1) + .deposit(NearToken::from_yoctonear(1)) .max_gas() .transact() .await @@ -1099,14 +1100,13 @@ pub mod workspace { } /// Deploys the EVM, deploys nep141 contract, and calls `set_silo_params` - #[allow(clippy::future_not_send)] async fn init_silo() -> SiloTestContext { // Deploy Aurora Engine let aurora = deploy_engine().await; // Create fallback account and evm address let fallback_account = aurora .root() - .create_subaccount("fallback", parse_near!("10 N")) + .create_subaccount("fallback", NearToken::from_near(10)) .await .unwrap(); let fallback_address = @@ -1124,7 +1124,7 @@ pub mod workspace { // Create `ft_owner` account and evm address let ft_owner = aurora .root() - .create_subaccount("ft_owner", parse_near!("10 N")) + .create_subaccount("ft_owner", NearToken::from_near(10)) .await .unwrap(); let ft_owner_address = @@ -1132,7 +1132,7 @@ pub mod workspace { let nep_141_account = aurora .root() - .create_subaccount(FT_ACCOUNT, parse_near!("10 N")) + .create_subaccount(FT_ACCOUNT, NearToken::from_near(10)) .await .unwrap(); @@ -1149,7 +1149,7 @@ pub mod workspace { "account_id": fallback_account.id(), "registration_only": None:: })) - .deposit(parse_near!("50 N")) + .deposit(NearToken::from_near(50)) .transact() .await .unwrap(); diff --git a/engine-tests/src/tests/standalone/call_tracer.rs b/engine-tests/src/tests/standalone/call_tracer.rs index 78d973d22..4e437efa4 100644 --- a/engine-tests/src/tests/standalone/call_tracer.rs +++ b/engine-tests/src/tests/standalone/call_tracer.rs @@ -1,4 +1,4 @@ -use crate::prelude::H256; +use crate::prelude::{H160, H256}; use crate::utils::solidity::erc20::{ERC20Constructor, ERC20}; use crate::utils::{self, standalone, Signer}; use aurora_engine_modexp::AuroraModExp; @@ -50,13 +50,32 @@ fn test_trace_precompile_direct_call() { runner.init_evm(); + let input = hex::decode("0000ca110000").unwrap(); + let precompile_cost = { + use aurora_engine_precompiles::Precompile; + let context = evm::Context { + address: H160::default(), + caller: H160::default(), + apparent_value: U256::zero(), + }; + let result = + aurora_engine_precompiles::identity::Identity.run(&input, None, &context, false); + result.unwrap().cost.as_u64() + }; let tx = aurora_engine_transactions::legacy::TransactionLegacy { nonce: signer.use_nonce().into(), gas_price: U256::zero(), gas_limit: u64::MAX.into(), - to: Some(aurora_engine_precompiles::random::RandomSeed::ADDRESS), + to: Some(aurora_engine_precompiles::identity::Identity::ADDRESS), value: Wei::zero(), - data: Vec::new(), + data: input.clone(), + }; + let intrinsic_cost = { + let signed_tx = + utils::sign_transaction(tx.clone(), Some(runner.chain_id), &signer.secret_key); + let kind = aurora_engine_transactions::EthTransactionKind::Legacy(signed_tx); + let norm_tx = aurora_engine_transactions::NormalizedEthTransaction::try_from(kind).unwrap(); + norm_tx.intrinsic_gas(&evm::Config::shanghai()).unwrap() }; let mut listener = CallTracer::default(); @@ -71,12 +90,12 @@ fn test_trace_precompile_direct_call() { let expected_trace = call_tracer::CallFrame { call_type: call_tracer::CallType::Call, from: utils::address_from_secret_key(&signer.secret_key), - to: Some(aurora_engine_precompiles::random::RandomSeed::ADDRESS), + to: Some(aurora_engine_precompiles::identity::Identity::ADDRESS), value: U256::zero(), gas: u64::MAX, - gas_used: 21000_u64, - input: Vec::new(), - output: [0u8; 32].to_vec(), + gas_used: intrinsic_cost + precompile_cost, + input: input.clone(), + output: input, error: None, calls: Vec::new(), }; diff --git a/engine-tests/src/tests/standalone/storage.rs b/engine-tests/src/tests/standalone/storage.rs index e72c79500..1e987c06f 100644 --- a/engine-tests/src/tests/standalone/storage.rs +++ b/engine-tests/src/tests/standalone/storage.rs @@ -273,6 +273,7 @@ fn test_transaction_index() { transaction: TransactionKind::Unknown, promise_data: Vec::new(), raw_input: Vec::new(), + action_hash: H256::default(), }; let tx_included = engine_standalone_storage::TransactionIncluded { block_hash, diff --git a/engine-tests/src/tests/standalone/sync.rs b/engine-tests/src/tests/standalone/sync.rs index 43f8a7476..1a14a1633 100644 --- a/engine-tests/src/tests/standalone/sync.rs +++ b/engine-tests/src/tests/standalone/sync.rs @@ -64,6 +64,7 @@ fn test_consume_deposit_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -102,6 +103,7 @@ fn test_consume_deposit_message() { // (which is `true` because the proof is valid in this case). promise_data: vec![Some(true.try_to_vec().unwrap())], raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -136,6 +138,7 @@ fn test_consume_deposit_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -170,6 +173,7 @@ fn test_consume_deploy_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -226,6 +230,7 @@ fn test_consume_deploy_erc20_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; // Deploy ERC-20 (this would be the flow for bridging a new NEP-141 to Aurora) @@ -268,6 +273,7 @@ fn test_consume_deploy_erc20_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; // Mint new tokens (via ft_on_transfer flow, same as the bridge) @@ -334,6 +340,7 @@ fn test_consume_ft_on_transfer_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -382,6 +389,7 @@ fn test_consume_call_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -436,6 +444,7 @@ fn test_consume_submit_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( diff --git a/engine-tests/src/tests/standalone/tracing.rs b/engine-tests/src/tests/standalone/tracing.rs index 616a574bf..bdcbcf72e 100644 --- a/engine-tests/src/tests/standalone/tracing.rs +++ b/engine-tests/src/tests/standalone/tracing.rs @@ -73,6 +73,7 @@ fn test_evm_tracing_with_storage() { transaction: engine_standalone_storage::sync::types::TransactionKind::Unknown, promise_data: Vec::new(), raw_input: Vec::new(), + action_hash: H256::default(), }, diff, maybe_result: Ok(None), diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs index 27fbd6730..14db5aa69 100644 --- a/engine-tests/src/tests/xcc.rs +++ b/engine-tests/src/tests/xcc.rs @@ -10,13 +10,17 @@ use aurora_engine_types::parameters::{ }; use aurora_engine_types::types::{Address, EthGas, NearGas, Wei, Yocto}; use aurora_engine_types::U256; +use aurora_engine_workspace::types::NearToken; use near_primitives::transaction::Action; use near_primitives_core::contract::ContractCode; use std::fs; use std::path::Path; -const WNEAR_AMOUNT: u128 = 10 * 50_000_000_000_000_000_000_000_000; -const STORAGE_AMOUNT: i128 = 50_000_000_000_000_000_000_000_000; +const WNEAR_AMOUNT: NearToken = NearToken::from_near(500); +const STORAGE_AMOUNT: NearToken = NearToken::from_near(50); + +const XCC_ROUTER_BASE_PATH: &str = "../etc/xcc-router"; +const XCC_ROUTER_VERSION_RELATIVE_PATH: &str = "src/VERSION"; #[test] #[allow(clippy::too_many_lines)] @@ -63,7 +67,7 @@ fn test_xcc_eth_gas_cost() { wnear_erc20.transfer_from( utils::address_from_secret_key(&signer.secret_key), Address::from_array([1u8; 20]), - U256::from(STORAGE_AMOUNT), + U256::from(STORAGE_AMOUNT.as_yoctonear()), nonce, ) }) @@ -327,7 +331,11 @@ fn deploy_erc20(runner: &mut AuroraRunner, signer: &utils::Signer) -> ERC20 { aurora_engine::parameters::CallArgs::V1(aurora_engine::parameters::FunctionCallArgsV1 { contract: address, input: contract - .mint(dest_address, WNEAR_AMOUNT.into(), U256::zero()) + .mint( + dest_address, + WNEAR_AMOUNT.as_yoctonear().into(), + U256::zero(), + ) .data, }); let result = runner.call("call", &engine_account, call_args.try_to_vec().unwrap()); @@ -344,19 +352,32 @@ fn approve_erc20( ) { let approve_result = runner .submit_with_signer(signer, |nonce| { - token.approve(spender, WNEAR_AMOUNT.into(), nonce) + token.approve(spender, WNEAR_AMOUNT.as_yoctonear().into(), nonce) }) .unwrap(); assert!(approve_result.status.is_ok()); } pub fn contract_bytes() -> Vec { - let base_path = Path::new("../etc").join("xcc-router"); + let base_path = Path::new(XCC_ROUTER_BASE_PATH); let output_path = base_path.join("target/wasm32-unknown-unknown/release/xcc_router.wasm"); utils::rust::compile(base_path); fs::read(output_path).unwrap() } +pub fn router_version() -> u32 { + let base_path = Path::new(XCC_ROUTER_BASE_PATH); + let file_path = base_path.join(XCC_ROUTER_VERSION_RELATIVE_PATH); + let version = fs::read_to_string(file_path).unwrap(); + version.trim().parse().unwrap() +} + +pub fn change_router_version(version: u32) { + let base_path = Path::new(XCC_ROUTER_BASE_PATH); + let file_path = base_path.join(XCC_ROUTER_VERSION_RELATIVE_PATH); + fs::write(file_path, format!("{version}\n")).unwrap(); +} + fn make_fib_promise(n: usize, account_id: &AccountId) -> NearPromise { if n == 0 { NearPromise::Simple(SimpleNearPromise::Create(PromiseCreateArgs { @@ -385,9 +406,10 @@ fn make_fib_promise(n: usize, account_id: &AccountId) -> NearPromise { pub mod workspace { use crate::tests::xcc::{check_fib_result, WNEAR_AMOUNT}; use crate::utils; + use crate::utils::solidity::erc20::{ERC20Constructor, ERC20}; use crate::utils::workspace::{ - create_sub_account, deploy_engine, deploy_erc20_from_nep_141, deploy_nep_141, - nep_141_balance_of, transfer_nep_141_to_erc_20, + create_sub_account, deploy_engine, deploy_engine_v331, deploy_erc20_from_nep_141, + deploy_nep_141, get_xcc_router_version, nep_141_balance_of, transfer_nep_141_to_erc_20, }; use aurora_engine_precompiles::xcc::cross_contract_call; use aurora_engine_transactions::legacy::TransactionLegacy; @@ -400,12 +422,13 @@ pub mod workspace { }; use aurora_engine_types::types::{Address, NearGas, Wei, Yocto}; use aurora_engine_types::U256; - use aurora_engine_workspace::{parse_near, EngineContract, RawContract}; + use aurora_engine_workspace::types::NearToken; + use aurora_engine_workspace::{EngineContract, RawContract}; use serde_json::json; use std::path::Path; - const STORAGE_AMOUNT: u128 = 50_000_000_000_000_000_000_000_000; - const ONE_NEAR: u128 = 10u128.pow(24); + const STORAGE_AMOUNT: NearToken = NearToken::from_near(50); + const ONE_NEAR: u128 = NearToken::from_near(1).as_yoctonear(); #[tokio::test] async fn test_xcc_external_fund() { @@ -431,7 +454,7 @@ pub mod workspace { let wnear_account = deploy_wnear(&aurora).await.unwrap(); // Fund XCC sub-account - let fund_amount = parse_near!("5 N"); + let fund_amount = NearToken::from_near(5); let result = aurora .fund_xcc_sub_account( signer_address, @@ -450,7 +473,10 @@ pub mod workspace { .get_balance(&sub_account_id.parse().unwrap()) .await .unwrap(); - assert_eq!((fund_amount - sub_account_balance) / ONE_NEAR, 0); + assert_eq!( + (fund_amount.as_yoctonear() - sub_account_balance) / ONE_NEAR, + 0 + ); // Do an XCC call. This XCC call is to the Aurora Engine itself to deploy an EVM contract, // but that is just for this test. The call could be to any contract to do any action. @@ -605,6 +631,152 @@ pub mod workspace { check_fib_result(&output, usize::try_from(n).unwrap()); } + #[allow(clippy::too_many_lines)] + #[tokio::test] + async fn test_xcc_upgrade() { + const DEPOSIT_AMOUNT: u128 = 1; + + // Deploy v3.3.1 Engine with the XCC router contract it had at the time. + let v1_bytes = std::fs::read("src/tests/res/xcc_router_v1.wasm").unwrap(); + let XccTestContext { + aurora, + mut signer, + signer_address, + chain_id, + wnear_account, + } = inner_init_xcc(v1_bytes, true).await.unwrap(); + + let router_account_id = create_router_account_id(&signer_address, &aurora); + + // Do XCC interaction to create router account + let promise = PromiseCreateArgs { + target_account_id: wnear_account.id(), + method: "near_deposit".into(), + args: b"{}".to_vec(), + attached_balance: Yocto::new(1), + attached_gas: NearGas::new(5_000_000_000_000), + }; + let promise_args = PromiseArgs::Create(promise); + let xcc_args = CrossContractCallArgs::Eager(promise_args); + submit_xcc_transaction(&xcc_args, &aurora, &mut signer, chain_id) + .await + .unwrap(); + assert_eq!( + nep_141_balance_of(&wnear_account, &router_account_id).await, + DEPOSIT_AMOUNT, + ); + + // Upgrade to latest engine code + aurora + .stage_upgrade(utils::AuroraRunner::get_engine_code()) + .max_gas() + .transact() + .await + .unwrap(); + aurora.deploy_upgrade().max_gas().transact().await.unwrap(); + + // Upgrade to Engine to have latest XCC + let current_xcc_version = super::router_version(); + aurora + .factory_update(super::contract_bytes()) + .max_gas() + .transact() + .await + .unwrap(); + + // Confirm that XCC v1 router account still works + submit_xcc_transaction(&xcc_args, &aurora, &mut signer, chain_id) + .await + .unwrap(); + assert_eq!( + nep_141_balance_of(&wnear_account, &router_account_id).await, + 2 * DEPOSIT_AMOUNT, + ); + + // Create new account on Aurora + let mut v2_signer = utils::Signer::random(); + let v2_signer_address = utils::address_from_secret_key(&v2_signer.secret_key); + let wnear_address = aurora.factory_get_wnear_address().await.unwrap().result; + let wnear_erc20 = { + let constructor = ERC20Constructor::load(); + let contract = constructor.0.deployed_at(wnear_address); + ERC20(contract) + }; + transfer_nep_141_to_erc_20( + &wnear_account, + &wnear_erc20, + &aurora.root(), + v2_signer_address, + WNEAR_AMOUNT.as_yoctonear(), + &aurora, + ) + .await + .unwrap(); + approve_xcc_precompile(&wnear_erc20, &aurora, chain_id, &mut v2_signer) + .await + .unwrap(); + + // Use XCC to create account with v2 router contract + let v2_router_account_id = create_router_account_id(&v2_signer_address, &aurora); + submit_xcc_transaction(&xcc_args, &aurora, &mut v2_signer, chain_id) + .await + .unwrap(); + assert_eq!( + nep_141_balance_of(&wnear_account, &v2_router_account_id).await, + DEPOSIT_AMOUNT, + ); + assert_eq!( + get_xcc_router_version(&aurora, &v2_router_account_id).await, + current_xcc_version, + ); + + // Upgrade to Engine to have fake XCC v3 + super::change_router_version(current_xcc_version + 1); + aurora + .factory_update(super::contract_bytes()) + .max_gas() + .transact() + .await + .unwrap(); + + // Use v2 XCC router account and confirm it is upgraded to v3 + submit_xcc_transaction(&xcc_args, &aurora, &mut v2_signer, chain_id) + .await + .unwrap(); + assert_eq!( + nep_141_balance_of(&wnear_account, &v2_router_account_id).await, + 2 * DEPOSIT_AMOUNT, + ); + assert_eq!( + get_xcc_router_version(&aurora, &v2_router_account_id).await, + current_xcc_version + 1, + ); + + // Upgrade Engine to have fake XCC v4 + super::change_router_version(current_xcc_version + 2); + aurora + .factory_update(super::contract_bytes()) + .max_gas() + .transact() + .await + .unwrap(); + + // Use XCC direct funding and confirm upgrade still happens + aurora + .fund_xcc_sub_account(v2_signer_address, Some(wnear_account.id())) + .max_gas() + .transact() + .await + .unwrap(); + assert_eq!( + get_xcc_router_version(&aurora, &v2_router_account_id).await, + current_xcc_version + 2, + ); + + // Restore XCC router version to not leave the git repo dirty + super::change_router_version(current_xcc_version); + } + #[allow(clippy::too_many_lines, clippy::future_not_send)] async fn test_xcc_precompile_common(is_scheduled: bool) { let XccTestContext { @@ -655,7 +827,7 @@ pub mod workspace { "receiver_id": router_account, "amount": format!("{transfer_amount}"), })) - .deposit(1) + .deposit(NearToken::from_yoctonear(1)) .transact() .await .unwrap(); @@ -751,13 +923,23 @@ pub mod workspace { ); } + /// Default XCC initialization (latest Aurora Engine code + latest XCC router code). + async fn init_xcc() -> anyhow::Result { + inner_init_xcc(super::contract_bytes(), false).await + } + /// Deploys the EVM, sets xcc router code, deploys wnear contract, bridges wnear into EVM, /// and calls `factory_set_wnear_address` - #[allow(clippy::future_not_send)] - async fn init_xcc() -> anyhow::Result { - let aurora = deploy_engine().await; + async fn inner_init_xcc( + xcc_wasm_bytes: Vec, + use_v331: bool, + ) -> anyhow::Result { + let aurora = if use_v331 { + deploy_engine_v331().await + } else { + deploy_engine().await + }; let chain_id = aurora.get_chain_id().await?.result.as_u64(); - let xcc_wasm_bytes = super::contract_bytes(); let result = aurora.factory_update(xcc_wasm_bytes).transact().await?; assert!(result.is_success()); @@ -773,7 +955,7 @@ pub mod workspace { &wnear_erc20, &aurora.root(), signer_address, - WNEAR_AMOUNT, + WNEAR_AMOUNT.as_yoctonear(), &aurora, ) .await @@ -787,18 +969,7 @@ pub mod workspace { let wnear_address = aurora.factory_get_wnear_address().await.unwrap().result; assert_eq!(wnear_address, wnear_erc20.0.address); - let approve_tx = wnear_erc20.approve( - cross_contract_call::ADDRESS, - WNEAR_AMOUNT.into(), - signer.use_nonce().into(), - ); - let signed_transaction = - utils::sign_transaction(approve_tx, Some(chain_id), &signer.secret_key); - let result = aurora - .submit(rlp::encode(&signed_transaction).to_vec()) - .transact() - .await?; - assert!(result.is_success()); + approve_xcc_precompile(&wnear_erc20, &aurora, chain_id, &mut signer).await?; Ok(XccTestContext { aurora, @@ -817,6 +988,39 @@ pub mod workspace { pub wnear_account: RawContract, } + fn create_router_account_id(signer_address: &Address, aurora: &EngineContract) -> AccountId { + let router_account = format!( + "{}.{}", + hex::encode(signer_address.as_bytes()), + aurora.id().as_ref() + ); + router_account.parse().unwrap() + } + + /// The signer approves the XCC precompile to spend its wrapped NEAR + async fn approve_xcc_precompile( + wnear_erc20: &ERC20, + aurora: &EngineContract, + chain_id: u64, + signer: &mut utils::Signer, + ) -> anyhow::Result<()> { + let approve_tx = wnear_erc20.approve( + cross_contract_call::ADDRESS, + WNEAR_AMOUNT.as_yoctonear().into(), + signer.use_nonce().into(), + ); + let signed_transaction = + utils::sign_transaction(approve_tx, Some(chain_id), &signer.secret_key); + let result = aurora + .submit(rlp::encode(&signed_transaction).to_vec()) + .transact() + .await?; + if !result.is_success() { + return Err(anyhow::Error::msg("Failed Approve transaction")); + }; + Ok(()) + } + async fn submit_xcc_transaction( xcc_args: &CrossContractCallArgs, aurora: &EngineContract, @@ -890,7 +1094,7 @@ pub mod workspace { let result = aurora .root() .call(&wrap_account.id(), "near_deposit") - .deposit(WNEAR_AMOUNT) + .deposit(WNEAR_AMOUNT.saturating_mul(3)) .transact() .await?; assert!(result.is_success(), "{result:?}"); @@ -906,7 +1110,8 @@ pub mod workspace { utils::rust::compile(base_path); std::fs::read(output_path)? }; - let fib_account = create_sub_account(&aurora.root(), "fib", parse_near!("50 N")).await?; + let fib_account = + create_sub_account(&aurora.root(), "fib", NearToken::from_near(50)).await?; fib_account .deploy(&fib_contract_bytes) .await diff --git a/engine-tests/src/utils/mod.rs b/engine-tests/src/utils/mod.rs index 2018c467a..a3361bfe5 100644 --- a/engine-tests/src/utils/mod.rs +++ b/engine-tests/src/utils/mod.rs @@ -98,6 +98,12 @@ pub struct AuroraRunner { // Empty by default. Can be set in tests if the transaction should be // executed as if it was a callback. pub promise_results: Vec, + // None by default. Can be set if the transaction requires randomness + // from the Near runtime. + // Note: this only sets the random value for the block, the random + // value available in the runtime is derived from this value and + // another hash that depends on the transaction itself. + pub block_random_value: Option, } /// Same as `AuroraRunner`, but consumes `self` on execution (thus preventing building on @@ -234,7 +240,12 @@ impl AuroraRunner { self.previous_logs = outcome.logs.clone(); if let Some(standalone_runner) = &mut self.standalone_runner { - standalone_runner.submit_raw(method_name, &self.context, &self.promise_results)?; + standalone_runner.submit_raw( + method_name, + &self.context, + &self.promise_results, + self.block_random_value, + )?; self.validate_standalone(); } @@ -539,8 +550,8 @@ impl AuroraRunner { outcome.return_data.as_value().unwrap() } - pub fn with_random_seed(mut self, random_seed: H256) -> Self { - self.context.random_seed = random_seed.as_bytes().to_vec(); + pub const fn with_block_random_value(mut self, random_seed: H256) -> Self { + self.block_random_value = Some(random_seed); self } @@ -600,6 +611,15 @@ impl AuroraRunner { std::fs::read(path).unwrap() } + pub fn get_engine_v331_code() -> Vec { + let path = if cfg!(feature = "ext-connector") { + "src/tests/res/aurora_silo_v3.3.1.wasm" + } else { + "src/tests/res/aurora_v3.3.1.wasm" + }; + std::fs::read(path).unwrap() + } + pub const fn get_default_chain_id() -> u64 { DEFAULT_CHAIN_ID } @@ -645,6 +665,7 @@ impl Default for AuroraRunner { previous_logs: Vec::new(), standalone_runner: Some(standalone::StandaloneRunner::default()), promise_results: Vec::new(), + block_random_value: None, } } } diff --git a/engine-tests/src/utils/standalone/mod.rs b/engine-tests/src/utils/standalone/mod.rs index b437d9661..d22592836 100644 --- a/engine-tests/src/utils/standalone/mod.rs +++ b/engine-tests/src/utils/standalone/mod.rs @@ -198,6 +198,7 @@ impl StandaloneRunner { method_name: &str, ctx: &near_vm_logic::VMContext, promise_results: &[PromiseResult], + block_random_value: Option, ) -> Result { let mut env = self.env.clone(); env.block_height = ctx.block_height; @@ -207,8 +208,8 @@ impl StandaloneRunner { env.current_account_id = ctx.current_account_id.as_ref().parse().unwrap(); env.signer_account_id = ctx.signer_account_id.as_ref().parse().unwrap(); env.prepaid_gas = NearGas::new(ctx.prepaid_gas); - if ctx.random_seed.len() == 32 { - env.random_seed = H256::from_slice(&ctx.random_seed); + if let Some(value) = block_random_value { + env.random_seed = value; } let promise_data: Vec<_> = promise_results @@ -239,6 +240,19 @@ impl StandaloneRunner { ); tx_msg.transaction = transaction_kind; + if ctx.random_seed.len() == 32 { + let runtime_random_value = { + use near_primitives_core::hash::CryptoHash; + let action_hash = CryptoHash(tx_msg.action_hash.0); + let random_seed = CryptoHash(env.random_seed.0); + near_primitives::utils::create_random_seed(u32::MAX, action_hash, random_seed) + }; + assert_eq!( + ctx.random_seed, runtime_random_value, + "Runtime random value should match computed value when it is specified" + ); + } + let outcome = sync::execute_transaction_message::(storage, tx_msg).unwrap(); self.cumulative_diff.append(outcome.diff.clone()); storage::commit(storage, &outcome); @@ -314,6 +328,13 @@ impl StandaloneRunner { PromiseResult::Successful(bytes) => Some(bytes.clone()), }) .collect(); + let action_hash = { + let mut bytes = Vec::with_capacity(32 + 32 + 8); + bytes.extend_from_slice(transaction_hash.as_bytes()); + bytes.extend_from_slice(block_hash.as_bytes()); + bytes.extend_from_slice(&(u64::MAX - u64::from(transaction_position)).to_le_bytes()); + aurora_engine_sdk::sha256(&bytes) + }; TransactionMessage { block_hash, near_receipt_id: transaction_hash, @@ -325,6 +346,7 @@ impl StandaloneRunner { transaction: TransactionKind::Unknown, promise_data, raw_input, + action_hash, } } diff --git a/engine-tests/src/utils/workspace.rs b/engine-tests/src/utils/workspace.rs index b4c9a4377..2476b21d2 100644 --- a/engine-tests/src/utils/workspace.rs +++ b/engine-tests/src/utils/workspace.rs @@ -10,16 +10,15 @@ use aurora_engine_types::parameters::connector::{FungibleTokenMetadata, Withdraw use aurora_engine_types::types::Address; use aurora_engine_types::U256; use aurora_engine_workspace::account::Account; -use aurora_engine_workspace::{parse_near, EngineContract, RawContract}; +use aurora_engine_workspace::{types::NearToken, EngineContract, RawContract}; use serde_json::json; const FT_PATH: &str = "src/tests/res/fungible_token.wasm"; -const STORAGE_AMOUNT: u128 = 50_000_000_000_000_000_000_000_000; +const STORAGE_AMOUNT: NearToken = NearToken::from_near(50); #[cfg(feature = "ext-connector")] const AURORA_ETH_CONNECTOR: &str = "aurora_eth_connector"; /// Deploy Aurora smart contract WITHOUT init external eth-connector. -#[allow(clippy::future_not_send)] pub async fn deploy_engine_with_code(code: Vec) -> EngineContract { let chain_id = AuroraRunner::get_default_chain_id(); aurora_engine_workspace::EngineContractBuilder::new() @@ -28,16 +27,23 @@ pub async fn deploy_engine_with_code(code: Vec) -> EngineContract { .with_code(code) .with_custodian_address("d045f7e19B2488924B97F9c145b5E51D0D895A65") .unwrap() - .with_root_balance(parse_near!("10000 N")) - .with_contract_balance(parse_near!("1000 N")) + .with_root_balance(NearToken::from_near(10000)) + .with_contract_balance(NearToken::from_near(1000)) .deploy_and_init() .await .unwrap() } -#[allow(clippy::let_and_return, clippy::future_not_send)] pub async fn deploy_engine() -> EngineContract { - let code = AuroraRunner::get_engine_code(); + inner_deploy_engine(AuroraRunner::get_engine_code()).await +} + +pub async fn deploy_engine_v331() -> EngineContract { + inner_deploy_engine(AuroraRunner::get_engine_v331_code()).await +} + +#[allow(clippy::let_and_return)] +async fn inner_deploy_engine(code: Vec) -> EngineContract { let contract = deploy_engine_with_code(code).await; #[cfg(feature = "ext-connector")] @@ -52,7 +58,10 @@ async fn init_eth_connector(aurora: &EngineContract) -> anyhow::Result<()> { let contract_bytes = get_aurora_eth_connector_contract(); let contract_account = aurora .root() - .create_subaccount(AURORA_ETH_CONNECTOR, 15 * STORAGE_AMOUNT) + .create_subaccount( + AURORA_ETH_CONNECTOR, + STORAGE_AMOUNT.checked_mul(15).unwrap(), + ) .await .unwrap(); let contract = contract_account.deploy(&contract_bytes).await.unwrap(); @@ -82,10 +91,20 @@ async fn init_eth_connector(aurora: &EngineContract) -> anyhow::Result<()> { Ok(()) } +pub async fn get_xcc_router_version(aurora: &EngineContract, xcc_account: &AccountId) -> u32 { + aurora + .root() + .view(xcc_account, "get_version") + .await + .unwrap() + .json::() + .unwrap() +} + pub async fn create_sub_account( master_account: &Account, account: &str, - balance: u128, + balance: NearToken, ) -> anyhow::Result { master_account.create_subaccount(account, balance).await } @@ -122,7 +141,7 @@ pub async fn transfer_nep_141_to_erc_20( "amount": amount.to_string(), "memo": "null", })) - .deposit(1) + .deposit(NearToken::from_yoctonear(1)) .transact() .await?; assert!(result.is_success(), "{result:?}"); @@ -208,7 +227,7 @@ pub async fn transfer_nep_141( "amount": amount.to_string(), "memo": "null", })) - .deposit(1) + .deposit(NearToken::from_yoctonear(1)) .transact() .await?; assert!(result.is_success()); diff --git a/engine-types/Cargo.toml b/engine-types/Cargo.toml index 611f81582..61183afad 100644 --- a/engine-types/Cargo.toml +++ b/engine-types/Cargo.toml @@ -33,4 +33,3 @@ std = ["borsh/std", "hex/std", "primitive-types/std", "primitive-types/serde", " # in other Rust contracts. contracts-std = ["borsh/std", "hex/std"] impl-serde = ["primitive-types/impl-serde"] -evm_bully = [] diff --git a/engine-types/src/parameters/engine.rs b/engine-types/src/parameters/engine.rs index 06d9f5f09..cb9ad34fd 100644 --- a/engine-types/src/parameters/engine.rs +++ b/engine-types/src/parameters/engine.rs @@ -20,13 +20,18 @@ pub enum NewCallArgs { } impl NewCallArgs { + /// Creates a `NewCallArs` from the provided bytes which could be represented + /// in JSON or Borsh format. Supporting arguments in JSON format starting from V4. pub fn deserialize(bytes: &[u8]) -> Result { - Self::try_from_slice(bytes).map_or_else( - |_| LegacyNewCallArgs::try_from_slice(bytes).map(Self::V1), - Ok, - ) + Self::try_from_json(bytes).or_else(|_| { + Self::try_from_slice(bytes).map_or_else( + |_| LegacyNewCallArgs::try_from_slice(bytes).map(Self::V1), + Ok, + ) + }) } + /// Returns a genesis hash of the Hashchain if present. #[must_use] pub const fn initial_hashchain(&self) -> Option { match self { @@ -34,6 +39,25 @@ impl NewCallArgs { Self::V1(_) | Self::V2(_) | Self::V3(_) => None, } } + + fn try_from_json(bytes: &[u8]) -> Result { + serde_json::from_slice::(bytes).map(Into::into) + } +} + +impl From for NewCallArgs { + fn from(value: NewCallJsonArgs) -> Self { + match value { + NewCallJsonArgs::V1(args) => Self::V4(args), + } + } +} + +/// JSON encoded new parameters. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum NewCallJsonArgs { + V1(NewCallArgsV4), } /// Old Borsh-encoded parameters for the `new` function. @@ -75,9 +99,10 @@ pub struct NewCallArgsV3 { pub key_manager: AccountId, } -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] pub struct NewCallArgsV4 { /// Chain id, according to the EIP-115 / ethereum-lists spec. + #[serde(with = "chain_id_deserialize")] pub chain_id: RawU256, /// Account which can upgrade this contract. /// Use empty to disable updatability. @@ -93,26 +118,18 @@ pub struct NewCallArgsV4 { /// Borsh-encoded parameters for the `set_owner` function. #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "impl-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "impl-serde", derive(Serialize, Deserialize))] pub struct SetOwnerArgs { pub new_owner: AccountId, } /// Borsh-encoded parameters for the `set_upgrade_delay_blocks` function. #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "impl-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "impl-serde", derive(Serialize, Deserialize))] pub struct SetUpgradeDelayBlocksArgs { pub upgrade_delay_blocks: u64, } -/// Borsh-encoded (genesis) account balance used by the `begin_chain` function. -#[cfg(feature = "evm_bully")] -#[derive(BorshSerialize, BorshDeserialize)] -pub struct AccountBalance { - pub address: Address, - pub balance: RawU256, -} - /// Borsh-encoded submit arguments used by the `submit_with_args` function. #[derive(Default, Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct SubmitArgs { @@ -125,40 +142,14 @@ pub struct SubmitArgs { } #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "impl-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "impl-serde", derive(Serialize, Deserialize))] pub struct StartHashchainArgs { pub block_height: u64, pub block_hashchain: RawH256, } -/// Borsh-encoded parameters for the `begin_chain` function. -#[cfg(feature = "evm_bully")] -#[derive(BorshSerialize, BorshDeserialize)] -pub struct BeginChainArgs { - pub chain_id: RawU256, - pub genesis_alloc: Vec, -} - -/// Borsh-encoded parameters for the `begin_block` function. -#[cfg(feature = "evm_bully")] -#[derive(BorshSerialize, BorshDeserialize)] -pub struct BeginBlockArgs { - /// The current block's hash (for replayer use). - pub hash: RawU256, - /// The current block's beneficiary address. - pub coinbase: Address, - /// The current block's timestamp (in seconds since the Unix epoch). - pub timestamp: RawU256, - /// The current block's number (the genesis block is number zero). - pub number: RawU256, - /// The current block's difficulty. - pub difficulty: RawU256, - /// The current block's gas limit. - pub gaslimit: RawU256, -} - /// Fungible token storage balance -#[derive(Default, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Serialize, Deserialize)] pub struct StorageBalance { pub total: Yocto, pub available: Yocto, @@ -182,7 +173,7 @@ pub struct PausePrecompilesCallArgs { } #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "impl-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "impl-serde", derive(Serialize, Deserialize))] pub struct ResultLog { pub address: Address, pub topics: Vec, @@ -191,7 +182,7 @@ pub struct ResultLog { /// The status of a transaction. #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] -#[cfg_attr(feature = "impl-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "impl-serde", derive(Serialize, Deserialize))] pub enum TransactionStatus { Succeed(Vec), Revert(Vec), @@ -237,7 +228,7 @@ impl AsRef<[u8]> for TransactionStatus { /// Borsh-encoded parameters for the `call`, `call_with_args`, `deploy_code`, /// and `deploy_with_input` methods. #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "impl-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "impl-serde", derive(Serialize, Deserialize))] pub struct SubmitResult { version: u8, pub status: TransactionStatus, @@ -351,6 +342,27 @@ pub struct RelayerKeyArgs { pub type FullAccessKeyArgs = RelayerKeyArgs; +mod chain_id_deserialize { + use crate::types::{u256_to_arr, RawU256}; + use primitive_types::U256; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + u64::deserialize(deserializer).map(|v| u256_to_arr(&(v.into()))) + } + + pub fn serialize(value: &RawU256, serializer: S) -> Result + where + S: Serializer, + { + let chain_id = U256::from_big_endian(value.as_slice()).low_u64(); + serializer.serialize_u64(chain_id) + } +} + pub mod errors { use crate::{account_id::ParseAccountError, String, ToString}; @@ -465,4 +477,30 @@ mod tests { assert_eq!(args.public_key, public_key); } + + #[test] + fn test_deserialize_new_call_args_json() { + let chain_id = 1_313_161_559; + let json = serde_json::json!({ + "chain_id": chain_id, + "owner_id": "aurora", + "upgrade_delay_blocks": 10, + "key_manager": "manager.near", + "initial_hashchain": null + }); + let arguments = NewCallArgs::deserialize(&serde_json::to_vec(&json).unwrap()); + let Ok(NewCallArgs::V4(arguments)) = arguments else { + panic!("Wrong type of arguments"); + }; + let value = serde_json::to_value(arguments).unwrap(); + assert_eq!(value.get("chain_id").unwrap().as_u64(), Some(chain_id)); + + let outdated = serde_json::json!({ + "chain_id": chain_id, + "owner_id": "aurora", + "upgrade_delay_blocks": 19 + }); + let arguments = NewCallArgs::deserialize(&serde_json::to_vec(&outdated).unwrap()); + assert!(matches!(arguments, Err(_))); + } } diff --git a/engine-types/src/parameters/xcc.rs b/engine-types/src/parameters/xcc.rs index 20a82093b..c64b7eb7b 100644 --- a/engine-types/src/parameters/xcc.rs +++ b/engine-types/src/parameters/xcc.rs @@ -1,6 +1,6 @@ use crate::account_id::AccountId; use crate::borsh::{self, BorshDeserialize, BorshSerialize}; -use crate::types::Address; +use crate::types::{Address, Yocto}; #[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] pub struct AddressVersionUpdateArgs { @@ -14,6 +14,12 @@ pub struct FundXccArgs { pub wnear_account_id: Option, } +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub struct WithdrawWnearToRouterArgs { + pub target: Address, + pub amount: Yocto, +} + /// Type wrapper for version of router contracts. #[derive( Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, BorshDeserialize, BorshSerialize, @@ -21,6 +27,8 @@ pub struct FundXccArgs { pub struct CodeVersion(pub u32); impl CodeVersion { + pub const ONE: Self = Self(1); + #[must_use] pub const fn increment(self) -> Self { Self(self.0 + 1) diff --git a/engine-workspace/Cargo.toml b/engine-workspace/Cargo.toml index d9ddd0b11..837fdd469 100644 --- a/engine-workspace/Cargo.toml +++ b/engine-workspace/Cargo.toml @@ -16,7 +16,6 @@ aurora-engine-types = { workspace = true, features = ["impl-serde", "std"] } anyhow.workspace = true near-gas.workspace = true near-sdk.workspace = true -near-units.workspace = true near-workspaces.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/engine-workspace/src/account.rs b/engine-workspace/src/account.rs index 31eb39716..ea9112f89 100644 --- a/engine-workspace/src/account.rs +++ b/engine-workspace/src/account.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use crate::contract::RawContract; use aurora_engine_types::public_key::PublicKey; -pub use near_units::parse_near; +use near_workspaces::types::NearToken; #[derive(Debug, Clone)] pub struct Account { @@ -39,7 +39,11 @@ impl Account { self.inner.id().as_str().parse().unwrap() } - pub async fn create_subaccount(&self, name: &str, balance: u128) -> anyhow::Result { + pub async fn create_subaccount( + &self, + name: &str, + balance: NearToken, + ) -> anyhow::Result { self.inner .create_subaccount(name) .initial_balance(balance) diff --git a/engine-workspace/src/lib.rs b/engine-workspace/src/lib.rs index 47f593aee..f84e5b5e2 100644 --- a/engine-workspace/src/lib.rs +++ b/engine-workspace/src/lib.rs @@ -2,11 +2,11 @@ use aurora_engine_types::account_id::AccountId; use aurora_engine_types::parameters::connector::FungibleTokenMetadata; use aurora_engine_types::types::address::Address; use aurora_engine_types::U256; +use near_workspaces::types::NearToken; use crate::node::Node; pub use crate::contract::{EngineContract, RawContract}; -pub use near_units::parse_near; pub mod account; pub mod contract; @@ -18,14 +18,14 @@ pub mod transaction; pub mod types { pub use near_workspaces::result::{ExecutionFinalResult, ExecutionOutcome}; - pub use near_workspaces::types::{KeyType, SecretKey}; + pub use near_workspaces::types::{KeyType, NearToken, SecretKey}; } const AURORA_LOCAL_CHAIN_ID: u64 = 1313161556; const OWNER_ACCOUNT_ID: &str = "aurora.root"; const PROVER_ACCOUNT_ID: &str = "prover.root"; -const ROOT_BALANCE: u128 = parse_near!("400 N"); -const CONTRACT_BALANCE: u128 = parse_near!("200 N"); +const ROOT_BALANCE: NearToken = NearToken::from_near(400); +const CONTRACT_BALANCE: NearToken = NearToken::from_near(200); #[derive(Debug)] pub struct EngineContractBuilder { @@ -35,8 +35,8 @@ pub struct EngineContractBuilder { prover_id: AccountId, custodian_address: Address, upgrade_delay_blocks: u64, - root_balance: u128, - contract_balance: u128, + root_balance: NearToken, + contract_balance: NearToken, ft_metadata: FungibleTokenMetadata, } @@ -94,12 +94,12 @@ impl EngineContractBuilder { self } - pub fn with_root_balance(mut self, balance: u128) -> Self { + pub fn with_root_balance(mut self, balance: NearToken) -> Self { self.root_balance = balance; self } - pub fn with_contract_balance(mut self, balance: u128) -> Self { + pub fn with_contract_balance(mut self, balance: NearToken) -> Self { self.contract_balance = balance; self } diff --git a/engine-workspace/src/macros.rs b/engine-workspace/src/macros.rs index f27a98f63..e29c04d6c 100644 --- a/engine-workspace/src/macros.rs +++ b/engine-workspace/src/macros.rs @@ -48,7 +48,7 @@ macro_rules! impl_call_return { self.0 = self.0.max_gas(); self } - pub fn deposit(mut self, deposit: u128) -> Self { + pub fn deposit(mut self, deposit: near_workspaces::types::NearToken) -> Self { self.0 = self.0.deposit(deposit); self } @@ -83,7 +83,7 @@ macro_rules! impl_call_return { self.0 = self.0.max_gas(); self } - pub fn deposit(mut self, deposit: u128) -> Self { + pub fn deposit(mut self, deposit: near_workspaces::types::NearToken) -> Self { self.0 = self.0.deposit(deposit); self } diff --git a/engine-workspace/src/node.rs b/engine-workspace/src/node.rs index 243c12584..00584cf82 100644 --- a/engine-workspace/src/node.rs +++ b/engine-workspace/src/node.rs @@ -1,6 +1,6 @@ use aurora_engine_types::account_id::AccountId; use near_workspaces::network::{NetworkClient, Sandbox}; -use near_workspaces::types::{KeyType, SecretKey}; +use near_workspaces::types::{KeyType, NearToken, SecretKey}; use near_workspaces::Worker; use std::str::FromStr; use std::time::Duration; @@ -15,7 +15,7 @@ pub struct Node { } impl Node { - pub async fn new(root: &str, root_balance: u128) -> anyhow::Result { + pub async fn new(root: &str, root_balance: NearToken) -> anyhow::Result { let worker = near_workspaces::sandbox().await?; let root = Self::create_root_account(&worker, root, root_balance).await?; @@ -36,14 +36,14 @@ impl Node { self.worker .view_account(&account_id) .await - .map(|d| d.balance) + .map(|d| d.balance.as_yoctonear()) .map_err(Into::into) } async fn create_root_account( worker: &Worker, root_acc_name: &str, - balance: u128, + balance: NearToken, ) -> anyhow::Result { use near_workspaces::AccessKey; diff --git a/engine-workspace/src/transaction.rs b/engine-workspace/src/transaction.rs index d74d99355..53d439ae9 100644 --- a/engine-workspace/src/transaction.rs +++ b/engine-workspace/src/transaction.rs @@ -2,6 +2,7 @@ use aurora_engine_types::borsh::BorshSerialize; use near_workspaces::result::ExecutionFinalResult; use near_workspaces::rpc::query::{Query, ViewFunction}; use near_workspaces::rpc::BoxFuture; +use near_workspaces::types::NearToken; use std::future::IntoFuture; pub struct ViewTransaction<'a> { @@ -72,7 +73,7 @@ impl CallTransaction { self } - pub fn deposit(mut self, deposit: u128) -> Self { + pub fn deposit(mut self, deposit: NearToken) -> Self { self.inner = self.inner.deposit(deposit); self } diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 4e81fba75..0695c4510 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aurora-engine" -version = "3.3.1" +version = "3.4.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -42,7 +42,6 @@ default = ["std"] std = ["aurora-engine-types/std", "aurora-engine-hashchain/std", "aurora-engine-sdk/std", "aurora-engine-precompiles/std", "aurora-engine-transactions/std", "ethabi/std", "evm/std", "hex/std", "rlp/std", "serde/std", "serde_json/std"] contract = ["aurora-engine-sdk/contract", "aurora-engine-precompiles/contract"] borsh-compat = ["aurora-engine-types/borsh-compat", "aurora-engine-hashchain/borsh-compat", "aurora-engine-sdk/borsh-compat", "aurora-engine-precompiles/borsh-compat"] -evm_bully = [] log = ["aurora-engine-sdk/log", "aurora-engine-precompiles/log"] tracing = ["evm/tracing"] error_refund = ["aurora-engine-precompiles/error_refund"] diff --git a/engine/src/contract_methods/connector/external.rs b/engine/src/contract_methods/connector/external.rs index c679722f8..132f665b6 100644 --- a/engine/src/contract_methods/connector/external.rs +++ b/engine/src/contract_methods/connector/external.rs @@ -23,6 +23,7 @@ use aurora_engine_types::parameters::connector::{ TransferCallCallArgs, WithdrawSerializeType, }; use aurora_engine_types::parameters::engine::errors::ParseArgsError; +use aurora_engine_types::parameters::engine::SubmitResult; use aurora_engine_types::parameters::{PromiseWithCallbackArgs, WithdrawCallArgs}; use aurora_engine_types::types::ZERO_WEI; use function_name::named; @@ -151,7 +152,7 @@ pub fn ft_on_transfer( io: I, env: &E, handler: &mut H, -) -> Result<(), ContractError> { +) -> Result, ContractError> { let current_account_id = env.current_account_id(); let predecessor_account_id = env.predecessor_account_id(); let mut engine: Engine<_, _> = Engine::new( @@ -164,13 +165,20 @@ pub fn ft_on_transfer( let args: NEP141FtOnTransferArgs = read_json_args(&io).map_err(Into::::into)?; let mut eth_connector = EthConnectorContract::init(io)?; - if predecessor_account_id == eth_connector.get_eth_connector_contract_account() { + let output = if predecessor_account_id == eth_connector.get_eth_connector_contract_account() { eth_connector.ft_on_transfer(&engine, &args)?; + None } else { - engine.receive_erc20_tokens(&predecessor_account_id, &args, ¤t_account_id, handler); - } + let result = engine.receive_erc20_tokens( + &predecessor_account_id, + &args, + ¤t_account_id, + handler, + ); + result.ok() + }; - Ok(()) + Ok(output) } #[allow(clippy::missing_const_for_fn)] diff --git a/engine/src/contract_methods/connector/internal.rs b/engine/src/contract_methods/connector/internal.rs index e183e6a7c..c9431df8e 100644 --- a/engine/src/contract_methods/connector/internal.rs +++ b/engine/src/contract_methods/connector/internal.rs @@ -25,6 +25,7 @@ use aurora_engine_types::parameters::connector::{ WithdrawResult, }; use aurora_engine_types::parameters::engine::errors::ParseArgsError; +use aurora_engine_types::parameters::engine::SubmitResult; use aurora_engine_types::parameters::{PromiseBatchAction, PromiseCreateArgs, WithdrawCallArgs}; use aurora_engine_types::storage::EthConnectorStorageId; use aurora_engine_types::types::address::error::AddressError; @@ -208,7 +209,7 @@ pub fn ft_on_transfer( io: I, env: &E, handler: &mut H, -) -> Result<(), ContractError> { +) -> Result, ContractError> { with_hashchain(io, env, function_name!(), |io| { let state = state::get_state(&io)?; require_running(&state)?; @@ -225,17 +226,19 @@ pub fn ft_on_transfer( let args: NEP141FtOnTransferArgs = serde_json::from_slice(&io.read_input().to_vec()) .map_err(Into::::into)?; - if predecessor_account_id == current_account_id { + let output = if predecessor_account_id == current_account_id { EthConnectorContract::init(io)?.ft_on_transfer(&engine, &args)?; + None } else { - engine.receive_erc20_tokens( + let result = engine.receive_erc20_tokens( &predecessor_account_id, &args, ¤t_account_id, handler, ); - } - Ok(()) + result.ok() + }; + Ok(output) }) } diff --git a/engine/src/contract_methods/connector/mod.rs b/engine/src/contract_methods/connector/mod.rs index 893f27a63..c8ae381cc 100644 --- a/engine/src/contract_methods/connector/mod.rs +++ b/engine/src/contract_methods/connector/mod.rs @@ -103,13 +103,13 @@ pub fn ft_on_transfer( io: I, env: &E, handler: &mut H, -) -> Result<(), ContractError> { +) -> Result, ContractError> { #[cfg(not(feature = "ext-connector"))] - internal::ft_on_transfer(io, env, handler)?; + let result = internal::ft_on_transfer(io, env, handler)?; #[cfg(feature = "ext-connector")] - external::ft_on_transfer(io, env, handler)?; + let result = external::ft_on_transfer(io, env, handler)?; - Ok(()) + Ok(result) } #[named] diff --git a/engine/src/contract_methods/evm_transactions.rs b/engine/src/contract_methods/evm_transactions.rs index f773ee6d6..4041def0c 100644 --- a/engine/src/contract_methods/evm_transactions.rs +++ b/engine/src/contract_methods/evm_transactions.rs @@ -56,17 +56,6 @@ pub fn call( let current_account_id = env.current_account_id(); let predecessor_account_id = env.predecessor_account_id(); - // During the XCC flow the Engine will call itself to move wNEAR - // to the user's sub-account. We do not want this move to happen - // if prior promises in the flow have failed. - if current_account_id == predecessor_account_id { - let check_promise: Result<(), &[u8]> = match handler.promise_result_check() { - Some(true) | None => Ok(()), - Some(false) => Err(b"ERR_CALLBACK_OF_FAILED_PROMISE"), - }; - check_promise?; - } - let mut engine: Engine<_, E, AuroraModExp> = Engine::new_with_state( state, predecessor_address(&predecessor_account_id), diff --git a/engine/src/contract_methods/xcc.rs b/engine/src/contract_methods/xcc.rs index 2a265b93d..dd0b15242 100644 --- a/engine/src/contract_methods/xcc.rs +++ b/engine/src/contract_methods/xcc.rs @@ -1,17 +1,69 @@ use crate::{ - contract_methods::{require_owner_only, require_running, ContractError}, + contract_methods::{predecessor_address, require_owner_only, require_running, ContractError}, + engine::Engine, errors, - hashchain::with_hashchain, + hashchain::{with_hashchain, with_logs_hashchain}, state, xcc, }; +use aurora_engine_modexp::AuroraModExp; use aurora_engine_sdk::{ env::Env, io::{StorageIntermediate, IO}, promise::PromiseHandler, }; -use aurora_engine_types::{borsh::BorshSerialize, types::Address}; +use aurora_engine_types::{ + account_id::AccountId, + borsh::BorshSerialize, + format, + parameters::{engine::SubmitResult, xcc::WithdrawWnearToRouterArgs}, + types::Address, +}; use function_name::named; +#[named] +pub fn withdraw_wnear_to_router( + io: I, + env: &E, + handler: &mut H, +) -> Result { + with_logs_hashchain(io, env, function_name!(), |io| { + let state = state::get_state(&io)?; + require_running(&state)?; + env.assert_private_call()?; + if matches!(handler.promise_result_check(), Some(false)) { + return Err(b"ERR_CALLBACK_OF_FAILED_PROMISE".into()); + } + let args: WithdrawWnearToRouterArgs = io.read_input_borsh()?; + let current_account_id = env.current_account_id(); + let recipient = AccountId::new(&format!( + "{}.{}", + args.target.encode(), + current_account_id.as_ref() + ))?; + let wnear_address = aurora_engine_precompiles::xcc::state::get_wnear_address(&io); + let mut engine: Engine<_, E, AuroraModExp> = Engine::new_with_state( + state, + predecessor_address(¤t_account_id), + current_account_id, + io, + env, + ); + let (result, ids) = xcc::withdraw_wnear_to_router( + &recipient, + args.amount, + wnear_address, + &mut engine, + handler, + )?; + if !result.status.is_ok() { + return Err(b"ERR_WITHDRAW_FAILED".into()); + } + let id = ids.last().ok_or(b"ERR_NO_PROMISE_CREATED")?; + handler.promise_return(*id); + Ok(result) + }) +} + #[named] pub fn factory_update(io: I, env: &E) -> Result<(), ContractError> { with_hashchain(io, env, function_name!(), |mut io| { diff --git a/engine/src/engine.rs b/engine/src/engine.rs index cf1e7bcae..9a4c08dd6 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -30,7 +30,7 @@ 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, KeyPrefix, PromiseArgs, PromiseCreateArgs, Vec, Wei, Yocto, + BTreeMap, BorshDeserialize, Cow, KeyPrefix, PromiseArgs, PromiseCreateArgs, Vec, Wei, Yocto, ERC20_DIGITS_SELECTOR, ERC20_MINT_SELECTOR, ERC20_NAME_SELECTOR, ERC20_SET_METADATA_SELECTOR, ERC20_SYMBOL_SELECTOR, H160, H256, U256, }; @@ -62,29 +62,6 @@ pub fn current_address(current_account_id: &AccountId) -> Address { aurora_engine_sdk::types::near_account_to_evm_address(current_account_id.as_bytes()) } -macro_rules! unwrap_res_or_finish { - ($e:expr, $output:expr, $io:expr) => { - match $e { - Ok(v) => v, - Err(_e) => { - #[cfg(feature = "log")] - sdk::log(crate::prelude::format!("{:?}", _e).as_str()); - $io.return_output($output); - return; - } - } - }; -} - -macro_rules! assert_or_finish { - ($e:expr, $output:expr, $io:expr) => { - if !$e { - $io.return_output($output); - return; - } - }; -} - #[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "impl-serde", derive(serde::Serialize))] pub struct EngineError { @@ -751,24 +728,34 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { args: &NEP141FtOnTransferArgs, current_account_id: &AccountId, handler: &mut P, - ) { + ) -> Result { + const INVALID_MESSAGE: &str = "receive_erc20_tokens invalid message"; + const UNKNOWN_NEP_141: &str = "receive_erc20_tokens unknown NEP-141"; + let str_amount = crate::prelude::format!("\"{}\"", args.amount); let output_on_fail = str_amount.as_bytes(); + let mut local_io = self.io; + let mut engine_err = |msg: &'static str| { + sdk::log!("{}", msg); + local_io.return_output(output_on_fail); + EngineError { + kind: EngineErrorKind::EvmError(ExitError::Other(Cow::Borrowed(msg))), + gas_used: 0, + } + }; // Parse message to determine recipient let mut recipient = { // Message format: // Recipient of the transaction - 40 characters (Address in hex) let message = args.msg.as_bytes(); - assert_or_finish!(message.len() >= 40, output_on_fail, self.io); - - Address::new(H160(unwrap_res_or_finish!( - unwrap_res_or_finish!(hex::decode(&message[..40]), output_on_fail, self.io) - .as_slice() - .try_into(), - output_on_fail, - self.io - ))) + if message.len() < 40 { + return Err(engine_err(INVALID_MESSAGE)); + } + let mut address_bytes = [0; 20]; + hex::decode_to_slice(&message[..40], &mut address_bytes) + .map_err(|_| engine_err(INVALID_MESSAGE))?; + Address::from_array(address_bytes) }; if let Some(fallback_address) = silo::get_erc20_fallback_address(&self.io) { @@ -777,21 +764,17 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { } }; - let erc20_token = Address::from_array(unwrap_res_or_finish!( - unwrap_res_or_finish!( - get_erc20_from_nep141(&self.io, token), - output_on_fail, - self.io - ) - .as_slice() - .try_into(), - output_on_fail, - self.io - )); + let erc20_token = { + let address_bytes: [u8; 20] = get_erc20_from_nep141(&self.io, token) + .ok() + .and_then(|bytes| bytes.as_slice().try_into().ok()) + .ok_or_else(|| engine_err(UNKNOWN_NEP_141))?; + Address::from_array(address_bytes) + }; let erc20_admin_address = current_address(current_account_id); - unwrap_res_or_finish!( - self.call( + let result = self + .call( &erc20_admin_address, &erc20_token, Wei::zero(), @@ -800,46 +783,49 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { Vec::new(), // TODO: are there values we should put here? handler, ) - .and_then(|submit_result| { - match submit_result.status { - TransactionStatus::Succeed(_) => Ok(()), - TransactionStatus::Revert(bytes) => { - let error_message = crate::prelude::format!( - "Reverted with message: {}", - crate::prelude::String::from_utf8_lossy(&bytes) - ); - Err(EngineError { - kind: EngineErrorKind::EvmError(ExitError::Other( - crate::prelude::Cow::from(error_message), - )), - gas_used: submit_result.gas_used, - }) - } - TransactionStatus::OutOfFund => Err(EngineError { - kind: EngineErrorKind::EvmError(ExitError::OutOfFund), - gas_used: submit_result.gas_used, - }), - TransactionStatus::OutOfOffset => Err(EngineError { - kind: EngineErrorKind::EvmError(ExitError::OutOfOffset), - gas_used: submit_result.gas_used, - }), - TransactionStatus::OutOfGas => Err(EngineError { - kind: EngineErrorKind::EvmError(ExitError::OutOfGas), + .and_then(|submit_result| match submit_result.status { + TransactionStatus::Succeed(_) => Ok(submit_result), + TransactionStatus::Revert(bytes) => { + let error_message = crate::prelude::format!( + "Reverted with message: {}", + crate::prelude::String::from_utf8_lossy(&bytes) + ); + Err(EngineError { + kind: EngineErrorKind::EvmError(ExitError::Other( + crate::prelude::Cow::from(error_message), + )), gas_used: submit_result.gas_used, - }), - TransactionStatus::CallTooDeep => Err(EngineError { - kind: EngineErrorKind::EvmError(ExitError::CallTooDeep), - gas_used: submit_result.gas_used, - }), + }) } - }), - output_on_fail, - self.io - ); + TransactionStatus::OutOfFund => Err(EngineError { + kind: EngineErrorKind::EvmError(ExitError::OutOfFund), + gas_used: submit_result.gas_used, + }), + TransactionStatus::OutOfOffset => Err(EngineError { + kind: EngineErrorKind::EvmError(ExitError::OutOfOffset), + gas_used: submit_result.gas_used, + }), + TransactionStatus::OutOfGas => Err(EngineError { + kind: EngineErrorKind::EvmError(ExitError::OutOfGas), + gas_used: submit_result.gas_used, + }), + TransactionStatus::CallTooDeep => Err(EngineError { + kind: EngineErrorKind::EvmError(ExitError::CallTooDeep), + gas_used: submit_result.gas_used, + }), + }) + .map_err(|e| { + sdk::log!("{:?}", e); + self.io.return_output(output_on_fail); + e + })?; - // TODO(marX) // Everything succeed so return "0" self.io.return_output(b"\"0\""); + + // Return SubmitResult so that it can be accessed in standalone engine. + // This is used to help with the indexing of bridge transactions. + Ok(result) } /// Read metadata of ERC-20 contract. @@ -1621,6 +1607,7 @@ where P: PromiseHandler, I: IO + Copy, { + let mut previous_promise: Option = None; logs.into_iter() .filter_map(|log| { if log.address == exit_to_near::ADDRESS.raw() @@ -1633,15 +1620,33 @@ where // Safety: this promise creation is safe because it does not come from // users directly. The exit precompiles only create promises which we // are able to execute without violating any security invariants. - unsafe { schedule_promise(handler, &promise) } + let id = unsafe { + match previous_promise { + Some(base_id) => { + schedule_promise_callback(handler, base_id, &promise) + } + None => schedule_promise(handler, &promise), + } + }; + previous_promise = Some(id); } PromiseArgs::Callback(promise) => { // Safety: This is safe because the promise data comes from our own // exit precompiles. See note above. - unsafe { - let base_id = schedule_promise(handler, &promise.base); + let base_id = unsafe { + match previous_promise { + Some(base_id) => schedule_promise_callback( + handler, + base_id, + &promise.base, + ), + None => schedule_promise(handler, &promise.base), + } + }; + let id = unsafe { schedule_promise_callback(handler, base_id, &promise.callback) - } + }; + previous_promise = Some(id); } PromiseArgs::Recursive(_) => { unreachable!("Exit precompiles do not produce recursive promises") @@ -1664,13 +1669,15 @@ where 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( + let id = crate::xcc::handle_precompile_promise( io, handler, + previous_promise, &promise, required_near, current_account_id, ); + previous_promise = Some(id); } } // do not pass on these "internal logs" to caller @@ -2289,7 +2296,9 @@ mod tests { engine .register_token(erc20_token, nep141_token.clone()) .unwrap(); - engine.receive_erc20_tokens(&nep141_token, &args, ¤t_account_id, &mut handler); + engine + .receive_erc20_tokens(&nep141_token, &args, ¤t_account_id, &mut handler) + .unwrap(); let storage = storage.borrow(); let actual_output = storage.output.as_slice(); diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 7eae00115..ade4376b7 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -76,8 +76,6 @@ pub unsafe fn on_alloc_error(_: core::alloc::Layout) -> ! { #[cfg(feature = "contract")] mod contract { use crate::engine::{self, Engine}; - #[cfg(feature = "evm_bully")] - use crate::parameters::{BeginBlockArgs, BeginChainArgs}; use crate::parameters::{GetErc20FromNep141CallArgs, GetStorageAtArgs, ViewCallArgs}; use crate::prelude::sdk::types::{SdkExpect, SdkUnwrap}; use crate::prelude::storage::{bytes_to_key, KeyPrefix}; @@ -389,6 +387,19 @@ mod contract { .sdk_unwrap(); } + /// A private function (only callable by the contract itself) used as part of the XCC flow. + /// This function uses the exit to Near precompile to move wNear from Aurora to a user's + /// XCC account. + #[no_mangle] + pub extern "C" fn withdraw_wnear_to_router() { + let io = Runtime; + let env = Runtime; + let mut handler = Runtime; + contract_methods::xcc::withdraw_wnear_to_router(io, &env, &mut handler) + .map_err(ContractError::msg) + .sdk_unwrap(); + } + /// Mirror existing ERC-20 token on the main Aurora contract. /// Notice: It works if the SILO mode is on. #[no_mangle] @@ -543,45 +554,6 @@ mod contract { .sdk_unwrap(); } - /// - /// BENCHMARKING METHODS - /// - #[cfg(feature = "evm_bully")] - #[no_mangle] - pub extern "C" fn begin_chain() { - use crate::prelude::U256; - let mut io = Runtime; - let mut state = state::get_state(&io).sdk_unwrap(); - if state.owner_id != io.predecessor_account_id() { - sdk::panic_utf8(errors::ERR_NOT_ALLOWED); - } - let args: BeginChainArgs = io.read_input_borsh().sdk_unwrap(); - state.chain_id = args.chain_id; - state::set_state(&mut io, &state).sdk_unwrap(); - // set genesis block balances - for account_balance in args.genesis_alloc { - engine::set_balance( - &mut io, - &account_balance.address, - &crate::prelude::Wei::new(U256::from(account_balance.balance)), - ); - } - // return new chain ID - io.return_output(&state::get_state(&io).sdk_unwrap().chain_id); - } - - #[cfg(feature = "evm_bully")] - #[no_mangle] - pub extern "C" fn begin_block() { - let io = Runtime; - let state = state::get_state(&io).sdk_unwrap(); - if state.owner_id != io.predecessor_account_id() { - sdk::panic_utf8(errors::ERR_NOT_ALLOWED); - } - let _args: BeginBlockArgs = io.read_input_borsh().sdk_unwrap(); - // TODO: https://github.com/aurora-is-near/aurora-engine/issues/2 - } - /// /// ETH-CONNECTOR /// diff --git a/engine/src/xcc.rs b/engine/src/xcc.rs index 6ca60f68f..fa11798a0 100644 --- a/engine/src/xcc.rs +++ b/engine/src/xcc.rs @@ -1,11 +1,14 @@ +use crate::engine::{Engine, EngineResult}; use crate::errors::ERR_SERIALIZE; -use crate::parameters::{CallArgs, FunctionCallArgsV2}; -use aurora_engine_precompiles::xcc::state::{self, ERR_MISSING_WNEAR_ADDRESS}; +use crate::parameters::{CallArgs, FunctionCallArgsV2, SubmitResult}; +use aurora_engine_modexp::ModExpAlgorithm; +use aurora_engine_precompiles::xcc::state::ERR_MISSING_WNEAR_ADDRESS; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::{StorageIntermediate, IO}; -use aurora_engine_sdk::promise::PromiseHandler; +use aurora_engine_sdk::promise::{PromiseHandler, PromiseId}; use aurora_engine_types::account_id::AccountId; -use aurora_engine_types::borsh::BorshSerialize; +use aurora_engine_types::borsh::{self, BorshDeserialize, BorshSerialize}; +use aurora_engine_types::parameters::xcc::WithdrawWnearToRouterArgs; 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}; @@ -16,11 +19,13 @@ pub use aurora_engine_types::parameters::xcc::{AddressVersionUpdateArgs, FundXcc 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 ERR_UPGRADE_ARG_SERIALIZATION: &str = "ERR_UPGRADE_ARG_SERIALIZATION"; pub const CODE_KEY: &[u8] = b"router_code"; /// Gas costs estimated from simulation tests. 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 UPGRADE_GAS: NearGas = NearGas::new(20_000_000_000_000); +pub const REFUND_GAS: NearGas = NearGas::new(5_000_000_000_000); pub const WITHDRAW_GAS: NearGas = NearGas::new(40_000_000_000_000); /// Solidity selector for the `withdrawToNear` function /// `https://www.4byte.directory/signatures/?bytes4_signature=0x6b351848` @@ -48,6 +53,13 @@ impl<'a> RouterCode<'a> { } } +/// Same as the corresponding struct in the xcc-router +#[derive(BorshDeserialize, BorshSerialize)] +pub struct DeployUpgradeParams { + pub code: Vec, + pub initialize_args: Vec, +} + pub fn fund_xcc_sub_account( io: &I, handler: &mut P, @@ -76,20 +88,9 @@ where // If account needs to be created and/or updated then include those actions. if let AddressVersionStatus::DeployNeeded { create_needed } = deploy_needed { - if create_needed { - if fund_amount < STORAGE_AMOUNT { - return Err(FundXccError::InsufficientBalance); - } - - promise_actions.push(PromiseAction::CreateAccount); - } - promise_actions.push(PromiseAction::Transfer { - amount: fund_amount, - }); - promise_actions.push(PromiseAction::DeployContract { - code: get_router_code(io).0.into_owned(), - }); - // Either we need to assume it is set in the Engine or we need to accept it as input. + let code = get_router_code(io).0.into_owned(); + // wnear_account is needed for initialization so we must assume it is set + // in the Engine or we need to accept it as input. let wnear_account = if let Some(wnear_account) = args.wnear_account_id { wnear_account } else { @@ -106,12 +107,36 @@ where wnear_account.as_ref(), create_needed, ); - promise_actions.push(PromiseAction::FunctionCall { - name: "initialize".into(), - args: init_args.into_bytes(), - attached_yocto: ZERO_YOCTO, - gas: INITIALIZE_GAS, - }); + if create_needed { + if fund_amount < STORAGE_AMOUNT { + return Err(FundXccError::InsufficientBalance); + } + + promise_actions.push(PromiseAction::CreateAccount); + promise_actions.push(PromiseAction::Transfer { + amount: fund_amount, + }); + promise_actions.push(PromiseAction::DeployContract { code }); + promise_actions.push(PromiseAction::FunctionCall { + name: "initialize".into(), + args: init_args.into_bytes(), + attached_yocto: ZERO_YOCTO, + gas: INITIALIZE_GAS, + }); + } else { + let deploy_args = DeployUpgradeParams { + code, + initialize_args: init_args.into_bytes(), + }; + promise_actions.push(PromiseAction::FunctionCall { + name: "deploy_upgrade".into(), + args: deploy_args + .try_to_vec() + .expect(ERR_UPGRADE_ARG_SERIALIZATION), + attached_yocto: fund_amount, + gas: UPGRADE_GAS + INITIALIZE_GAS, + }); + } } else { // No matter what include the transfer of the funding amount promise_actions.push(PromiseAction::Transfer { @@ -154,10 +179,12 @@ where pub fn handle_precompile_promise( io: &I, handler: &mut P, + base_id: Option, promise: &PromiseCreateArgs, required_near: Yocto, current_account_id: &AccountId, -) where +) -> PromiseId +where P: PromiseHandler, I: IO + Copy, { @@ -185,16 +212,8 @@ pub fn handle_precompile_promise( 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::DeployContract { - code: get_router_code(io).0.into_owned(), - }); - // After the deployment we call the contract's initialize function + let code = get_router_code(io).0.into_owned(); + // After the deployment we will call the contract's initialize function let wnear_address = get_wnear_address(io); let wnear_account = crate::engine::nep141_erc20_map(*io) .lookup_right(&crate::engine::ERC20Address(wnear_address)) @@ -204,12 +223,33 @@ pub fn handle_precompile_promise( 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, - }); + if *create_needed { + promise_actions.push(PromiseAction::CreateAccount); + promise_actions.push(PromiseAction::Transfer { + amount: STORAGE_AMOUNT, + }); + promise_actions.push(PromiseAction::DeployContract { code }); + promise_actions.push(PromiseAction::FunctionCall { + name: "initialize".into(), + args: init_args.into_bytes(), + attached_yocto: ZERO_YOCTO, + gas: INITIALIZE_GAS, + }); + } else { + let deploy_args = DeployUpgradeParams { + code, + initialize_args: init_args.into_bytes(), + }; + promise_actions.push(PromiseAction::FunctionCall { + name: "deploy_upgrade".into(), + args: deploy_args + .try_to_vec() + .expect(ERR_UPGRADE_ARG_SERIALIZATION), + attached_yocto: ZERO_YOCTO, + gas: UPGRADE_GAS + INITIALIZE_GAS, + }); + } + let batch = PromiseBatchAction { target_account_id: promise.target_account_id.clone(), actions: promise_actions, @@ -218,7 +258,12 @@ pub fn handle_precompile_promise( // (not the main engine account), and the actions performed are only (1) create it // for the first time and/or (2) deploy the code from our storage (i.e. the deployed // code is controlled by us, not the user). - let promise_id = unsafe { handler.promise_create_batch(&batch) }; + let promise_id = unsafe { + match base_id { + Some(id) => handler.promise_attach_batch_callback(id, &batch), + None => handler.promise_create_batch(&batch), + } + }; // Add a callback here to update the version of the account let args = AddressVersionUpdateArgs { address: sender, @@ -237,7 +282,7 @@ pub fn handle_precompile_promise( // metadata that has just been deployed above. unsafe { Some(handler.promise_attach_callback(promise_id, &callback)) } } - AddressVersionStatus::UpToDate => None, + AddressVersionStatus::UpToDate => base_id, }; // 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 @@ -248,15 +293,13 @@ pub fn handle_precompile_promise( 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_args = WithdrawWnearToRouterArgs { + target: sender, + amount: required_near, + }; let withdraw_call = PromiseCreateArgs { target_account_id: current_account_id.clone(), - method: "call".into(), + method: "withdraw_wnear_to_router".into(), args: withdraw_call_args.try_to_vec().unwrap(), attached_balance: ZERO_YOCTO, attached_gas: WITHDRAW_GAS, @@ -276,22 +319,20 @@ pub fn handle_precompile_promise( 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, - }; - // Safety: This call is safe because the router's `unwrap_and_refund_storage` method - // does not violate any security invariants. It only interacts with the wrap.near contract - // to obtain NEAR from WNEAR. - unsafe { Some(handler.promise_attach_callback(id, &unwrap_call)) } + if refund_needed { + let refund_call = PromiseCreateArgs { + target_account_id: promise.target_account_id.clone(), + method: "send_refund".into(), + args: Vec::new(), + attached_balance: ZERO_YOCTO, + attached_gas: REFUND_GAS, + }; + // Safety: This call is safe because the router's `send_refund` method + // does not violate any security invariants. It only sends NEAR back to this contract. + unsafe { Some(handler.promise_attach_callback(id, &refund_call)) } + } else { + Some(id) + } }; // 3. Finally we can do the call the user wanted to do. @@ -299,12 +340,12 @@ pub fn handle_precompile_promise( // user directly. The XCC precompile will only construct promises that target the `execute` // and `schedule` methods of the user's router contract. Therefore, the user cannot have // the engine make arbitrary calls. - let _promise_id = unsafe { + unsafe { 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 @@ -336,6 +377,32 @@ pub fn set_code_version_of_address(io: &mut I, address: &Address, version io.write_storage(&key, &value_bytes); } +pub fn withdraw_wnear_to_router( + recipient: &AccountId, + amount: Yocto, + wnear_address: Address, + engine: &mut Engine, + handler: &mut H, +) -> EngineResult<(SubmitResult, Vec)> { + let mut interceptor = PromiseInterceptor::new(handler); + let withdraw_call_args = withdraw_wnear_call_args(recipient, amount, wnear_address); + let result = engine.call_with_args(withdraw_call_args, &mut interceptor)?; + Ok((result, interceptor.promises)) +} + +#[must_use] +pub fn withdraw_wnear_call_args( + recipient: &AccountId, + amount: Yocto, + wnear_address: Address, +) -> CallArgs { + CallArgs::V2(FunctionCallArgsV2 { + contract: wnear_address, + value: [0u8; 32], + input: withdraw_to_near_args(recipient, amount), + }) +} + #[derive(Debug, Clone, Copy)] pub enum FundXccError { InsufficientBalance, @@ -381,6 +448,11 @@ impl AddressVersionStatus { None => Self::DeployNeeded { create_needed: true, }, + Some(version) if version == CodeVersion::ONE => { + // It is impossible to upgrade the initial XCC routers because + // they lack the upgrade method. + Self::UpToDate + } Some(version) if version < latest_code_version => Self::DeployNeeded { create_needed: false, }, @@ -390,13 +462,90 @@ impl AddressVersionStatus { } fn withdraw_to_near_args(recipient: &AccountId, amount: Yocto) -> Vec { + let recipient_with_msg = format!("{recipient}:unwrap"); let args = ethabi::encode(&[ - ethabi::Token::Bytes(recipient.as_bytes().to_vec()), + ethabi::Token::Bytes(recipient_with_msg.into_bytes()), ethabi::Token::Uint(U256::from(amount.as_u128())), ]); [&WITHDRAW_TO_NEAR_SELECTOR, args.as_slice()].concat() } +/// A `PromiseHandler` that remembers all the `PromiseIds` it creates. +/// This is used to make a promise the return value of a function even +/// if the promise was not captured in the code where the handler is used. +/// For example, this can capture the promises created by the exit precompiles. +struct PromiseInterceptor<'a, H> { + inner: &'a mut H, + promises: Vec, +} + +impl<'a, H> PromiseInterceptor<'a, H> { + fn new(inner: &'a mut H) -> Self { + Self { + inner, + promises: Vec::new(), + } + } +} + +impl<'a, H: PromiseHandler> PromiseHandler for PromiseInterceptor<'a, H> { + type ReadOnly = H::ReadOnly; + + fn promise_results_count(&self) -> u64 { + self.inner.promise_results_count() + } + + fn promise_result(&self, index: u64) -> Option { + self.inner.promise_result(index) + } + + unsafe fn promise_create_call(&mut self, args: &PromiseCreateArgs) -> PromiseId { + let id = self.inner.promise_create_call(args); + self.promises.push(id); + id + } + + unsafe fn promise_create_and_combine(&mut self, args: &[PromiseCreateArgs]) -> PromiseId { + let id = self.inner.promise_create_and_combine(args); + self.promises.push(id); + id + } + + unsafe fn promise_attach_callback( + &mut self, + base: PromiseId, + callback: &PromiseCreateArgs, + ) -> PromiseId { + let id = self.inner.promise_attach_callback(base, callback); + self.promises.push(id); + id + } + + unsafe fn promise_create_batch(&mut self, args: &PromiseBatchAction) -> PromiseId { + let id = self.inner.promise_create_batch(args); + self.promises.push(id); + id + } + + unsafe fn promise_attach_batch_callback( + &mut self, + base: PromiseId, + args: &PromiseBatchAction, + ) -> PromiseId { + let id = self.inner.promise_attach_batch_callback(base, args); + self.promises.push(id); + id + } + + fn promise_return(&mut self, promise: PromiseId) { + self.inner.promise_return(promise); + } + + fn read_only(&self) -> Self::ReadOnly { + self.inner.read_only() + } +} + #[cfg(test)] mod tests { use aurora_engine_types::{account_id::AccountId, types::Yocto, U256}; @@ -404,6 +553,7 @@ mod tests { #[test] fn test_withdraw_to_near_encoding() { let recipient: AccountId = "some_account.near".parse().unwrap(); + let recipient_with_msg = format!("{recipient}:unwrap"); let amount = Yocto::new(1332654); #[allow(deprecated)] let withdraw_function = ethabi::Function { @@ -426,7 +576,7 @@ mod tests { }; let expected_tx_data = withdraw_function .encode_input(&[ - ethabi::Token::Bytes(recipient.as_bytes().to_vec()), + ethabi::Token::Bytes(recipient_with_msg.into_bytes()), ethabi::Token::Uint(U256::from(amount.as_u128())), ]) .unwrap(); diff --git a/etc/eth-contracts/yarn.lock b/etc/eth-contracts/yarn.lock index 34c658be7..7e4380121 100644 --- a/etc/eth-contracts/yarn.lock +++ b/etc/eth-contracts/yarn.lock @@ -2691,7 +2691,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3: +bn.js@^5.0.0, bn.js@^5.1.2, bn.js@^5.1.3: version "5.2.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== @@ -2824,7 +2824,7 @@ browserify-des@^1.0.0: inherits "^2.0.1" safe-buffer "^5.1.2" -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: +browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== @@ -2833,19 +2833,19 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + version "4.2.2" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" + integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" + bn.js "^5.2.1" + browserify-rsa "^4.1.0" create-hash "^1.2.0" create-hmac "^1.1.7" - elliptic "^6.5.3" + elliptic "^6.5.4" inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" + parse-asn1 "^5.1.6" + readable-stream "^3.6.2" + safe-buffer "^5.2.1" browserslist@^3.2.6: version "3.2.8" @@ -4044,7 +4044,7 @@ elliptic@6.5.3: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" -elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3: +elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -8373,7 +8373,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.5: +parse-asn1@^5.0.0, parse-asn1@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== @@ -8957,6 +8957,15 @@ readable-stream@^3.0.6, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.0.15: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" diff --git a/etc/xcc-router/src/VERSION b/etc/xcc-router/src/VERSION new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/etc/xcc-router/src/VERSION @@ -0,0 +1 @@ +2 diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index c8763bc5f..d55a3ced4 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -4,7 +4,7 @@ use aurora_engine_types::parameters::{ }; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; -use near_sdk::json_types::{U128, U64}; +use near_sdk::json_types::U64; use near_sdk::BorshStorageKey; use near_sdk::{ env, near_bindgen, AccountId, Gas, PanicOnDefault, Promise, PromiseIndex, PromiseResult, @@ -22,18 +22,14 @@ enum StorageKey { Map, } -const CURRENT_VERSION: u32 = 1; +const INITIALIZE: &str = "initialize"; +const CURRENT_VERSION: u32 = std::include!("VERSION"); const ERR_ILLEGAL_CALLER: &str = "ERR_ILLEGAL_CALLER"; -/// Gas cost estimated from mainnet data. Cost seems to consistently be 3 Tgas, but we add a -/// little more to be safe. Example: -/// https://explorer.mainnet.near.org/transactions/3U9SKbGKM3MchLa2hLTNuYLdErcEDneJGbGv1cHZXuvE#HsHabUdJ7DRJcseNa4GQTYwm8KtbB4mqsq2AUssJWWv6 -const WNEAR_WITHDRAW_GAS: Gas = Gas(5_000_000_000_000); +const INITIALIZE_GAS: Gas = Gas(15_000_000_000_000); /// Gas cost estimated from mainnet data. Example: /// https://explorer.mainnet.near.org/transactions/5NbZ7SfrodNxeLcSkCmLAEdbZfbkk9cjqz3zSDwktKrk#D7un3c3Nxv7Ee3JpQSKiM97LbwCDFPbMo5iLoijGPXPM const WNEAR_REGISTER_GAS: Gas = Gas(5_000_000_000_000); -/// Gas cost estimated from simulation tests. -const REFUND_GAS: Gas = Gas(5_000_000_000_000); /// Registration amount computed from FT token source code, see /// https://github.com/near/near-sdk-rs/blob/master/near-contract-standards/src/fungible_token/core_impl.rs#L50 /// https://github.com/near/near-sdk-rs/blob/master/near-contract-standards/src/fungible_token/storage_impl.rs#L101 @@ -41,6 +37,12 @@ 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; +#[derive(BorshDeserialize, BorshSerialize)] +pub struct DeployUpgradeParams { + pub code: Vec, + pub initialize_args: Vec, +} + #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] pub struct Router { @@ -75,7 +77,9 @@ impl Router { parent.set(&caller); } Some(parent) => { - if caller != parent { + // Allow self-calls to `initialize` also. + // This happens during the upgrade flow. + if (caller != parent) && (caller != env::current_account_id()) { env::panic_str(ERR_ILLEGAL_CALLER); } } @@ -109,13 +113,17 @@ impl Router { } } + pub fn get_version(&self) -> u32 { + self.version.get().unwrap_or_default() + } + /// 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(); + self.assert_preconditions(); let promise_id = Router::promise_create(promise); env::promise_return(promise_id) @@ -123,7 +131,7 @@ impl Router { /// Similar security considerations here as for `execute`. pub fn schedule(&mut self, #[serializer(borsh)] promise: PromiseArgs) { - self.require_parent_caller(); + self.assert_preconditions(); let nonce = self.nonce.get().unwrap_or_default(); self.scheduled_promises.insert(&nonce, &promise); @@ -146,66 +154,56 @@ impl Router { 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, + /// Allows the parent contract to trigger an update to the logic of this contract + /// (by deploying a new contract to this account); + #[payable] + pub fn deploy_upgrade(&mut self, #[serializer(borsh)] args: DeployUpgradeParams) { + self.assert_preconditions(); + + let promise_id = env::promise_batch_create(&env::current_account_id()); + env::promise_batch_action_deploy_contract(promise_id, &args.code); + env::promise_batch_action_function_call( + promise_id, + INITIALIZE, + &args.initialize_args, + 0, + INITIALIZE_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); + env::promise_return(promise_id); } - #[private] pub fn send_refund(&self) -> Promise { - let parent = self - .parent - .get() - .unwrap_or_else(|| env::panic_str("ERR_CONTRACT_NOT_INITIALIZED")); + let parent = self.get_parent().unwrap_or_else(env_panic); + + require_caller(&parent) + .and_then(|_| require_no_failed_promises()) + .unwrap_or_else(env_panic); 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); - } - // Any method that can only be called by the parent should also only be executed if - // the parent's execution was successful. - let num_promises = env::promise_results_count(); - for index in 0..num_promises { - if let PromiseResult::Failed | PromiseResult::NotReady = env::promise_result(index) { - env::panic_str("ERR_CALLBACK_OF_FAILED_PROMISE"); - } - } + fn get_parent(&self) -> Result { + self.parent.get().ok_or(Error::ContractNotInitialized) + } + + /// Checks the following preconditions: + /// 1. Contract is initialized + /// 2. predecessor_account_id == self.parent + /// 3. There are no failed promise results + /// These preconditions must be checked on methods where are important for + /// the security of the contract (e.g. `execute`). + fn require_preconditions(&self) -> Result<(), Error> { + let parent = self.get_parent()?; + require_caller(&parent)?; + require_no_failed_promises()?; + Ok(()) + } + + /// Panics if any of the preconditions checked in `require_preconditions` are not met. + fn assert_preconditions(&self) { + self.require_preconditions().unwrap_or_else(env_panic); } fn promise_create(promise: PromiseArgs) -> PromiseIndex { @@ -367,3 +365,41 @@ fn to_sdk_pk(key: &aurora_engine_types::parameters::NearPublicKey) -> near_sdk:: // Unwrap should be safe because we only encode valid public keys data.try_into().unwrap() } + +fn require_caller(caller: &AccountId) -> Result<(), Error> { + if caller != &env::predecessor_account_id() { + return Err(Error::IllegalCaller); + } + Ok(()) +} + +fn require_no_failed_promises() -> Result<(), Error> { + let num_promises = env::promise_results_count(); + for index in 0..num_promises { + if let PromiseResult::Failed | PromiseResult::NotReady = env::promise_result(index) { + return Err(Error::CallbackOfFailedPromise); + } + } + Ok(()) +} + +fn env_panic(e: Error) -> T { + env::panic_str(e.as_ref()) +} + +#[derive(Debug)] +enum Error { + ContractNotInitialized, + IllegalCaller, + CallbackOfFailedPromise, +} + +impl AsRef for Error { + fn as_ref(&self) -> &str { + match self { + Self::ContractNotInitialized => "ERR_CONTRACT_NOT_INITIALIZED", + Self::IllegalCaller => ERR_ILLEGAL_CALLER, + Self::CallbackOfFailedPromise => "ERR_CALLBACK_OF_FAILED_PROMISE", + } + } +} diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index ce11f20d6..80a69f7e1 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -1,3 +1,4 @@ #!/bin/bash -cargo install --no-default-features --force cargo-make +rustup toolchain add stable +cargo +stable install --no-default-features --force cargo-make cargo make --profile "$1" build-docker-inner diff --git a/scripts/docker-xcc-router-entrypoint.sh b/scripts/docker-xcc-router-entrypoint.sh index 539096517..6d50edf5d 100755 --- a/scripts/docker-xcc-router-entrypoint.sh +++ b/scripts/docker-xcc-router-entrypoint.sh @@ -1,3 +1,4 @@ #!/bin/bash -cargo install --no-default-features --force cargo-make +rustup toolchain add stable +cargo +stable install --no-default-features --force cargo-make cargo make --profile "$1" build-xcc-router-docker-inner