diff --git a/Cargo.lock b/Cargo.lock index c191c75f4..4df8ba37e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,7 +327,7 @@ dependencies = [ [[package]] name = "aurora-engine" -version = "2.9.2" +version = "2.10.0" dependencies = [ "aurora-engine-modexp", "aurora-engine-precompiles", @@ -6641,6 +6641,15 @@ dependencies = [ "libc", ] +[[package]] +name = "xcc_router" +version = "1.0.0" +dependencies = [ + "aurora-engine-sdk", + "aurora-engine-types", + "near-sdk", +] + [[package]] name = "zeroize" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index fea7dc915..0cc34575c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ members = [ "engine-transactions", "engine-types", "engine-workspace", + "etc/xcc-router", ] exclude = [ @@ -89,7 +90,6 @@ exclude = [ "etc/tests/self-contained-5bEgfRQ", "etc/tests/fibonacci", "etc/tests/modexp-bench", - "etc/xcc-router", ] [profile.release] diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 874942438..9ba0ddedb 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -9,7 +9,7 @@ use aurora_engine_types::{ account_id::AccountId, borsh::{BorshDeserialize, BorshSerialize}, format, - parameters::{CrossContractCallArgs, PromiseCreateArgs}, + parameters::{CrossContractCallArgs, PromiseArgsWithSender, PromiseCreateArgs}, types::{balance::ZERO_YOCTO, Address, EthGas, NearGas}, vec, Cow, Vec, H160, H256, U256, }; @@ -138,12 +138,18 @@ impl HandleBasedPrecompile for CrossContractCall { let call_gas = call.total_gas(); let attached_near = call.total_near(); let callback_count = call.promise_count() - 1; + let refund_costs = call.refund_costs(); let router_exec_cost = costs::ROUTER_EXEC_BASE - + NearGas::new(callback_count * costs::ROUTER_EXEC_PER_CALLBACK.as_u64()); + + NearGas::new(callback_count * costs::ROUTER_EXEC_PER_CALLBACK.as_u64()) + + NearGas::new(refund_costs); + let args = PromiseArgsWithSender { + sender: *context.address.as_fixed_bytes(), + args: call, + }; let promise = PromiseCreateArgs { target_account_id, method: consts::ROUTER_EXEC_NAME.into(), - args: call + args: args .try_to_vec() .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, attached_balance: ZERO_YOCTO, @@ -153,10 +159,14 @@ impl HandleBasedPrecompile for CrossContractCall { } CrossContractCallArgs::Delayed(call) => { let attached_near = call.total_near(); + let args = PromiseArgsWithSender { + sender: *context.address.as_fixed_bytes(), + args: call, + }; let promise = PromiseCreateArgs { target_account_id, method: consts::ROUTER_SCHEDULE_NAME.into(), - args: call + args: args .try_to_vec() .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?, attached_balance: ZERO_YOCTO, diff --git a/engine-tests/src/tests/standalone/call_tracer.rs b/engine-tests/src/tests/standalone/call_tracer.rs index 4880063a2..8143147f3 100644 --- a/engine-tests/src/tests/standalone/call_tracer.rs +++ b/engine-tests/src/tests/standalone/call_tracer.rs @@ -3,6 +3,7 @@ use crate::utils::solidity::erc20::{ERC20Constructor, ERC20}; use crate::utils::{self, standalone, Signer}; use aurora_engine_modexp::AuroraModExp; use aurora_engine_types::borsh::BorshSerialize; +use aurora_engine_types::parameters::PromiseArgsWithSender; use aurora_engine_types::{ parameters::{CrossContractCallArgs, PromiseArgs, PromiseCreateArgs}, storage, @@ -341,7 +342,10 @@ fn test_trace_precompiles_with_subcalls() { attached_balance: Yocto::new(1), attached_gas: NearGas::new(100_000_000_000_000), }; - let xcc_args = CrossContractCallArgs::Delayed(PromiseArgs::Create(promise)); + let xcc_args = CrossContractCallArgs::Delayed(PromiseArgsWithSender { + sender: signer_address, + args: PromiseArgs::Create(promise), + }); let tx = aurora_engine_transactions::legacy::TransactionLegacy { nonce: signer.use_nonce().into(), gas_price: U256::zero(), diff --git a/engine-types/src/parameters/promise.rs b/engine-types/src/parameters/promise.rs index 8820bbaa3..734eda914 100644 --- a/engine-types/src/parameters/promise.rs +++ b/engine-types/src/parameters/promise.rs @@ -8,6 +8,13 @@ use borsh::{maybestd::io, BorshDeserialize, BorshSerialize}; #[cfg(feature = "borsh-compat")] use borsh_compat::{self as borsh, maybestd::io, BorshDeserialize, BorshSerialize}; +#[must_use] +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct PromiseArgsWithSender { + pub sender: [u8; 20], + pub args: PromiseArgs, +} + #[must_use] #[derive(Debug, BorshSerialize, BorshDeserialize)] pub enum PromiseArgs { @@ -16,6 +23,14 @@ pub enum PromiseArgs { Recursive(NearPromise), } +fn refund_costs(method: &str) -> u64 { + match method { + "ft_transfer" => 14_000_000_000_000, + "ft_transfer_call" => 39_000_000_000_000, + _ => 0, + } +} + impl PromiseArgs { /// Counts the total number of promises this call creates (including callbacks). #[must_use] @@ -27,6 +42,18 @@ impl PromiseArgs { } } + /// Counts the total gas cost of potentail refunds this call creates. + #[must_use] + pub fn refund_costs(&self) -> u64 { + match self { + Self::Create(call) => refund_costs(call.method.as_str()), + Self::Callback(cb) => { + refund_costs(cb.base.method.as_str()) + refund_costs(cb.callback.method.as_str()) + } + Self::Recursive(p) => p.refund_count(), + } + } + #[must_use] pub fn total_gas(&self) -> NearGas { match self { @@ -53,6 +80,26 @@ pub enum SimpleNearPromise { } impl SimpleNearPromise { + #[must_use] + pub fn refund_count(&self) -> u64 { + match self { + Self::Create(call) => refund_costs(call.method.as_str()), + Self::Batch(batch) => { + let total: u64 = batch + .actions + .iter() + .filter_map(|a| { + if let PromiseAction::FunctionCall { name, .. } = a { + Some(refund_costs(name.as_str())) + } else { + None + } + }) + .sum(); + total + } + } + } #[must_use] pub fn total_gas(&self) -> NearGas { match self { @@ -117,6 +164,14 @@ impl NearPromise { Self::And(ps) => ps.iter().map(Self::promise_count).sum(), } } + #[must_use] + pub fn refund_count(&self) -> u64 { + match self { + Self::Simple(x) => x.refund_count(), + Self::Then { base, callback } => base.refund_count() + callback.refund_count(), + Self::And(ps) => ps.iter().map(Self::refund_count).sum(), + } + } #[must_use] pub fn total_gas(&self) -> NearGas { diff --git a/etc/xcc-router/Cargo.toml b/etc/xcc-router/Cargo.toml index c9fe89312..dc233fb5d 100644 --- a/etc/xcc-router/Cargo.toml +++ b/etc/xcc-router/Cargo.toml @@ -18,6 +18,9 @@ panic = "abort" aurora-engine-types = { path = "../../engine-types", default-features = false, features = ["borsh-compat"] } near-sdk = "4.1" +[dev-dependencies] +aurora-engine-sdk = { workspace = true, features = ["std"] } + [features] default = [] all-promise-actions = [] diff --git a/etc/xcc-router/src/lib.rs b/etc/xcc-router/src/lib.rs index c8763bc5f..95adcd9a1 100644 --- a/etc/xcc-router/src/lib.rs +++ b/etc/xcc-router/src/lib.rs @@ -1,10 +1,11 @@ use aurora_engine_types::parameters::{ - NearPromise, PromiseAction, PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs, - SimpleNearPromise, + NearPromise, PromiseAction, PromiseArgs, PromiseArgsWithSender, PromiseCreateArgs, + PromiseWithCallbackArgs, SimpleNearPromise, }; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; use near_sdk::json_types::{U128, U64}; +use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::BorshStorageKey; use near_sdk::{ env, near_bindgen, AccountId, Gas, PanicOnDefault, Promise, PromiseIndex, PromiseResult, @@ -22,6 +23,29 @@ enum StorageKey { Map, } +#[derive(Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +struct FtTransferCallArgs { + pub receiver_id: AccountId, + pub amount: U128, + pub memo: Option, + pub msg: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +struct FtResolveTransferCallArgs { + pub sender: [u8; 20], + pub token_id: AccountId, + pub amount: U128, +} + +// #[derive(Serialize)] +// #[serde(crate = "near_sdk::serde")] +// struct FtResolveTransferCallArgs { +// pub amount: U128, +// } + const CURRENT_VERSION: u32 = 1; const ERR_ILLEGAL_CALLER: &str = "ERR_ILLEGAL_CALLER"; @@ -34,6 +58,8 @@ const WNEAR_WITHDRAW_GAS: Gas = Gas(5_000_000_000_000); 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); +const FT_RESOLVE_TRANSFER_CALL_GAS: Gas = Gas(39_000_000_000_000); +const FT_TRANSFER_CALL_GAS: Gas = Gas(31_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 @@ -52,7 +78,7 @@ pub struct Router { /// This allows multiple promises to be scheduled before any of them are executed. nonce: LazyOption, /// The storage for the scheduled promises. - scheduled_promises: LookupMap, + scheduled_promises: LookupMap, /// Account ID for the wNEAR contract. wnear_account: AccountId, } @@ -114,7 +140,7 @@ impl Router { /// 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) { + pub fn execute(&self, #[serializer(borsh)] promise: PromiseArgsWithSender) { self.require_parent_caller(); let promise_id = Router::promise_create(promise); @@ -122,7 +148,7 @@ impl Router { } /// Similar security considerations here as for `execute`. - pub fn schedule(&mut self, #[serializer(borsh)] promise: PromiseArgs) { + pub fn schedule(&mut self, #[serializer(borsh)] promise: PromiseArgsWithSender) { self.require_parent_caller(); let nonce = self.nonce.get().unwrap_or_default(); @@ -186,6 +212,40 @@ impl Router { Promise::new(parent).transfer(REFUND_AMOUNT) } + + #[private] + pub fn ft_resolve_transfer_call(&self, sender: [u8; 20], token_id: AccountId, amount: U128) { + let used_amount = match env::promise_result(0) { + PromiseResult::Successful(value) => { + near_sdk::serde_json::from_slice::(&value).unwrap() + } + PromiseResult::Failed => 0.into(), + PromiseResult::NotReady => panic!(), + }; + let unused_amount = U128(amount.0 - used_amount.0); + let address = sender.map(|c| format!("{:02x?}", c)).concat(); + near_sdk::log!("amount {}", amount.0); + near_sdk::log!("used_amount {}", used_amount.0); + near_sdk::log!("unused_amount {}", unused_amount.0); + near_sdk::log!("address {}", &address); + near_sdk::log!("self.parent {}", self.parent.get().unwrap()); + if unused_amount.0 > 0 { + let promise_idx = env::promise_create( + token_id, + "ft_transfer_call", + &near_sdk::serde_json::to_vec(&FtTransferCallArgs { + receiver_id: self.parent.get().unwrap(), + amount: unused_amount, + msg: address, + memo: Some("refund".to_string()), + }) + .unwrap(), + 1u128, + FT_TRANSFER_CALL_GAS, + ); + env::promise_return(promise_idx) + } + } } impl Router { @@ -208,16 +268,18 @@ impl Router { } } - fn promise_create(promise: PromiseArgs) -> PromiseIndex { - match promise { - PromiseArgs::Create(call) => Self::base_promise_create(&call), - PromiseArgs::Callback(cb) => Self::cb_promise_create(&cb), - PromiseArgs::Recursive(p) => Self::recursive_promise_create(&p), + fn promise_create(promise: PromiseArgsWithSender) -> PromiseIndex { + near_sdk::log!("PROMISE {:?}", &promise); + near_sdk::log!("prepaid_gas {}", near_sdk::env::prepaid_gas().0); + match promise.args { + PromiseArgs::Create(call) => Self::base_promise_create(promise.sender, &call), + PromiseArgs::Callback(cb) => Self::cb_promise_create(promise.sender, &cb), + PromiseArgs::Recursive(p) => Self::recursive_promise_create(promise.sender, &p), } } - fn cb_promise_create(promise: &PromiseWithCallbackArgs) -> PromiseIndex { - let base = Self::base_promise_create(&promise.base); + fn cb_promise_create(sender: [u8; 20], promise: &PromiseWithCallbackArgs) -> PromiseIndex { + let base = Self::base_promise_create(sender, &promise.base); let promise = &promise.callback; env::promise_then( @@ -230,20 +292,49 @@ impl Router { ) } - fn base_promise_create(promise: &PromiseCreateArgs) -> PromiseIndex { - env::promise_create( - near_sdk::AccountId::new_unchecked(promise.target_account_id.to_string()), + fn base_promise_create(sender: [u8; 20], promise: &PromiseCreateArgs) -> PromiseIndex { + // let gas = if promise.method == "ft_transfer_call" { + // (promise.attached_gas.as_u64() - FT_RESOLVE_TRANSFER_CALL_GAS.0).into() + // } else { + // promise.attached_gas.as_u64().into() + // }; + + let account_id = near_sdk::AccountId::new_unchecked(promise.target_account_id.to_string()); + let mut res = env::promise_create( + account_id.clone(), promise.method.as_str(), &promise.args, promise.attached_balance.as_u128(), + // gas, promise.attached_gas.as_u64().into(), - ) + ); + + if promise.method == "ft_transfer_call" { + let FtTransferCallArgs { amount, .. } = + near_sdk::serde_json::from_slice::(&promise.args).unwrap(); + + res = env::promise_then( + res, + env::current_account_id(), + "ft_resolve_transfer_call", + &near_sdk::serde_json::to_vec(&FtResolveTransferCallArgs { + sender, + token_id: account_id, + amount, + }) + .unwrap(), + 0, + FT_RESOLVE_TRANSFER_CALL_GAS, + ); + } + + res } - fn recursive_promise_create(promise: &NearPromise) -> PromiseIndex { + fn recursive_promise_create(sender: [u8; 20], promise: &NearPromise) -> PromiseIndex { match promise { NearPromise::Simple(x) => match x { - SimpleNearPromise::Create(call) => Self::base_promise_create(call), + SimpleNearPromise::Create(call) => Self::base_promise_create(sender, call), SimpleNearPromise::Batch(batch) => { let target = near_sdk::AccountId::new_unchecked(batch.target_account_id.to_string()); @@ -253,7 +344,7 @@ impl Router { } }, NearPromise::Then { base, callback } => { - let base_index = Self::recursive_promise_create(base); + let base_index = Self::recursive_promise_create(sender, base); match callback { SimpleNearPromise::Create(call) => env::promise_then( base_index, @@ -275,7 +366,7 @@ impl Router { NearPromise::And(promises) => { let indices: Vec = promises .iter() - .map(Self::recursive_promise_create) + .map(|p| Self::recursive_promise_create(sender, p)) .collect(); env::promise_and(&indices) } diff --git a/etc/xcc-router/src/tests.rs b/etc/xcc-router/src/tests.rs index ab9ee2a04..92f7233f3 100644 --- a/etc/xcc-router/src/tests.rs +++ b/etc/xcc-router/src/tests.rs @@ -1,6 +1,10 @@ use super::Router; -use aurora_engine_types::parameters::{PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs}; -use aurora_engine_types::types::{NearGas, Yocto}; +use aurora_engine_sdk as sdk; +use aurora_engine_types::parameters::{ + PromiseArgs, PromiseArgsWithSender, PromiseCreateArgs, PromiseWithCallbackArgs, +}; +use aurora_engine_types::types::{Address, NearGas, Yocto}; +use near_sdk::borsh::BorshSerialize; use near_sdk::mock::VmAction; use near_sdk::test_utils::test_env::{alice, bob, carol}; use near_sdk::test_utils::{self, VMContextBuilder}; @@ -10,7 +14,7 @@ const WNEAR_ACCOUNT: &str = "wrap.near"; #[test] fn test_initialize() { - let (parent, contract) = create_contract(); + let (parent, _signer, contract) = create_contract(); assert_eq!(contract.parent.get().unwrap(), parent); } @@ -18,7 +22,7 @@ fn test_initialize() { /// `initialize` should be able to be called multiple times without resetting the state. #[test] fn test_reinitialize() { - let (_parent, mut contract) = create_contract(); + let (_parent, _signer, mut contract) = create_contract(); let nonce = 8; contract.nonce.set(&nonce); @@ -32,7 +36,7 @@ fn test_reinitialize() { #[test] #[should_panic] fn test_reinitialize_wrong_caller() { - let (parent, contract) = create_contract(); + let (parent, _signer, contract) = create_contract(); assert_eq!(contract.parent.get().unwrap(), parent); drop(contract); @@ -46,7 +50,7 @@ fn test_reinitialize_wrong_caller() { #[test] #[should_panic] fn test_execute_wrong_caller() { - let (_parent, contract) = create_contract(); + let (_parent, signer, contract) = create_contract(); let promise = PromiseCreateArgs { target_account_id: bob().as_str().parse().unwrap(), @@ -59,12 +63,15 @@ fn test_execute_wrong_caller() { testing_env!(VMContextBuilder::new() .predecessor_account_id(bob()) .build()); - contract.execute(PromiseArgs::Create(promise)); + contract.execute(PromiseArgsWithSender { + sender: signer.raw().into(), + args: PromiseArgs::Create(promise), + }); } #[test] fn test_execute() { - let (_parent, contract) = create_contract(); + let (_parent, signer, contract) = create_contract(); let promise = PromiseCreateArgs { target_account_id: bob().as_str().parse().unwrap(), @@ -74,7 +81,10 @@ fn test_execute() { attached_gas: NearGas::new(100_000_000_000_000), }; - contract.execute(PromiseArgs::Create(promise.clone())); + contract.execute(PromiseArgsWithSender { + sender: signer.raw().into(), + args: PromiseArgs::Create(promise.clone()), + }); let mut receipts = test_utils::get_created_receipts(); assert_eq!(receipts.len(), 1); @@ -89,7 +99,7 @@ fn test_execute() { #[test] fn test_execute_callback() { - let (_parent, contract) = create_contract(); + let (_parent, signer, contract) = create_contract(); let promise = PromiseWithCallbackArgs { base: PromiseCreateArgs { @@ -108,7 +118,10 @@ fn test_execute_callback() { }, }; - contract.execute(PromiseArgs::Callback(promise.clone())); + contract.execute(PromiseArgsWithSender { + sender: signer.raw().into(), + args: PromiseArgs::Callback(promise.clone()), + }); let receipts = test_utils::get_created_receipts(); assert_eq!(receipts.len(), 2); @@ -122,7 +135,7 @@ fn test_execute_callback() { #[test] #[should_panic] fn test_schedule_wrong_caller() { - let (_parent, mut contract) = create_contract(); + let (_parent, signer, mut contract) = create_contract(); let promise = PromiseCreateArgs { target_account_id: bob().as_str().parse().unwrap(), @@ -135,12 +148,16 @@ fn test_schedule_wrong_caller() { testing_env!(VMContextBuilder::new() .predecessor_account_id(bob()) .build()); - contract.schedule(PromiseArgs::Create(promise)); + + contract.schedule(PromiseArgsWithSender { + sender: signer.raw().into(), + args: PromiseArgs::Create(promise), + }); } #[test] fn test_schedule_and_execute() { - let (_parent, mut contract) = create_contract(); + let (_parent, signer, mut contract) = create_contract(); let promise = PromiseCreateArgs { target_account_id: bob().as_str().parse().unwrap(), @@ -150,7 +167,10 @@ fn test_schedule_and_execute() { attached_gas: NearGas::new(100_000_000_000_000), }; - contract.schedule(PromiseArgs::Create(promise.clone())); + contract.schedule(PromiseArgsWithSender { + sender: signer.raw().into(), + args: PromiseArgs::Create(promise.clone()), + }); // no promise actually create yet let receipts = test_utils::get_created_receipts(); @@ -159,7 +179,10 @@ fn test_schedule_and_execute() { // promise stored and nonce incremented instead assert_eq!(contract.nonce.get().unwrap(), 1); let stored_promise = match contract.scheduled_promises.get(&0) { - Some(PromiseArgs::Create(promise)) => promise, + Some(PromiseArgsWithSender { + args: PromiseArgs::Create(promise), + .. + }) => promise, _ => unreachable!(), }; assert_eq!(stored_promise, promise); @@ -199,13 +222,18 @@ fn validate_function_call_action(actions: &[VmAction], promise: PromiseCreateArg ); } -fn create_contract() -> (near_sdk::AccountId, Router) { +fn create_contract() -> (near_sdk::AccountId, Address, Router) { + // let mut signer = Signer::random(); + // let signer_address = utils::address_from_secret_key(&signer.secret_key); + let parent = alice(); testing_env!(VMContextBuilder::new() .current_account_id(format!("some_address.{}", parent).try_into().unwrap()) .predecessor_account_id(parent.clone()) .build()); + let hash = sdk::keccak(&parent.try_to_vec().unwrap()); + let signer = Address::try_from_slice(&hash[12..]).unwrap(); let contract = Router::initialize(WNEAR_ACCOUNT.parse().unwrap(), false); - (parent, contract) + (parent, signer, contract) }