aurora_engine_precompiles/
xcc.rs

1//! Cross contract call precompile.
2//!
3//! Allow Aurora users interacting with NEAR smart contracts using cross contract call primitives.
4
5use crate::{utils, HandleBasedPrecompile, PrecompileOutput};
6use aurora_engine_sdk::io::IO;
7use aurora_engine_types::{
8    account_id::AccountId,
9    borsh::{self, BorshDeserialize},
10    format,
11    parameters::{CrossContractCallArgs, PromiseCreateArgs},
12    str,
13    types::{balance::ZERO_YOCTO, Address, EthGas, NearGas},
14    vec, Cow, Vec, H160, H256, U256,
15};
16use aurora_evm::backend::Log;
17use aurora_evm::executor::stack::{PrecompileFailure, PrecompileHandle};
18use aurora_evm::ExitError;
19
20pub mod costs {
21    use crate::prelude::types::{EthGas, NearGas};
22
23    /// Base EVM gas cost for calling this precompile.
24    /// Value obtained from the following methodology:
25    /// 1. Estimate the cost of calling this precompile in terms of NEAR gas.
26    ///    This is done by calling the precompile with inputs of different lengths
27    ///    and performing a linear regression to obtain a function
28    ///    `NEAR_gas = CROSS_CONTRACT_CALL_BASE + (input_length) * (CROSS_CONTRACT_CALL_BYTE)`.
29    /// 2. Convert the NEAR gas cost into an EVM gas cost using the conversion ratio below
30    ///    (`CROSS_CONTRACT_CALL_NEAR_GAS`).
31    ///
32    /// This process is done in the `test_xcc_eth_gas_cost` test in
33    /// `engine-tests/src/tests/xcc.rs`.
34    pub const CROSS_CONTRACT_CALL_BASE: EthGas = EthGas::new(343_650);
35    /// Additional EVM gas cost per bytes of input given.
36    /// See `CROSS_CONTRACT_CALL_BASE` for estimation methodology.
37    pub const CROSS_CONTRACT_CALL_BYTE: EthGas = EthGas::new(4);
38    /// EVM gas cost per NEAR gas attached to the created promise.
39    /// This value is derived from the gas report `https://hackmd.io/@birchmd/Sy4piXQ29`
40    /// The units on this quantity are `NEAR Gas / EVM Gas`.
41    /// The report gives a value `0.175 T(NEAR_gas) / k(EVM_gas)`. To convert the units to
42    /// `NEAR Gas / EVM Gas`, we simply multiply `0.175 * 10^12 / 10^3 = 175 * 10^6`.
43    pub const CROSS_CONTRACT_CALL_NEAR_GAS: u64 = 175_000_000;
44
45    pub const ROUTER_EXEC_BASE: NearGas = NearGas::new(7_000_000_000_000);
46    pub const ROUTER_EXEC_PER_CALLBACK: NearGas = NearGas::new(12_000_000_000_000);
47    pub const ROUTER_SCHEDULE: NearGas = NearGas::new(5_000_000_000_000);
48}
49
50mod consts {
51    pub(super) const ERR_INVALID_INPUT: &str = "ERR_INVALID_XCC_INPUT";
52    pub(super) const ERR_SERIALIZE: &str = "ERR_XCC_CALL_SERIALIZE";
53    pub(super) const ERR_STATIC: &str = "ERR_INVALID_IN_STATIC";
54    pub(super) const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE";
55    pub(super) const ERR_XCC_ACCOUNT_ID: &str = "ERR_FAILED_TO_CREATE_XCC_ACCOUNT_ID";
56    pub(super) const ROUTER_EXEC_NAME: &str = "execute";
57    pub(super) const ROUTER_SCHEDULE_NAME: &str = "schedule";
58    /// Solidity selector for the ERC-20 transferFrom function
59    /// `https://www.4byte.directory/signatures/?bytes4_signature=0x23b872dd`
60    pub(super) const TRANSFER_FROM_SELECTOR: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd];
61}
62
63pub struct CrossContractCall<I> {
64    io: I,
65    engine_account_id: AccountId,
66}
67
68impl<I> CrossContractCall<I> {
69    pub const fn new(engine_account_id: AccountId, io: I) -> Self {
70        Self {
71            io,
72            engine_account_id,
73        }
74    }
75}
76
77pub mod cross_contract_call {
78    use aurora_engine_types::{
79        types::{make_address, Address},
80        H256,
81    };
82
83    /// NEAR Cross Contract Call precompile address
84    ///
85    /// Address: `0x516cded1d16af10cad47d6d49128e2eb7d27b372`
86    /// This address is computed as: `&keccak("nearCrossContractCall")[12..]`
87    pub const ADDRESS: Address = make_address(0x516cded1, 0xd16af10cad47d6d49128e2eb7d27b372);
88
89    /// Sentinel value used to indicate the following topic field is how much NEAR the
90    /// cross-contract call will require.
91    pub const AMOUNT_TOPIC: H256 = crate::make_h256(
92        0x0072657175697265645f6e656172,
93        0x0072657175697265645f6e656172,
94    );
95}
96
97impl<I: IO> HandleBasedPrecompile for CrossContractCall<I> {
98    #[allow(clippy::too_many_lines)]
99    fn run_with_handle(
100        &self,
101        handle: &mut impl PrecompileHandle,
102    ) -> Result<PrecompileOutput, PrecompileFailure> {
103        let input = handle.input();
104        let target_gas = handle.gas_limit().map(EthGas::new);
105        let context = handle.context();
106        utils::validate_no_value_attached_to_precompile(context.apparent_value)?;
107        let is_static = handle.is_static();
108
109        // This only includes the cost we can easily derive without parsing the input.
110        // This allows failing fast without wasting computation on parsing.
111        let input_len = u64::try_from(input.len()).map_err(utils::err_usize_conv)?;
112        let mut cost =
113            costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input_len;
114        let check_cost = |cost: EthGas| -> Result<(), PrecompileFailure> {
115            if let Some(target_gas) = target_gas {
116                if cost > target_gas {
117                    return Err(PrecompileFailure::Error {
118                        exit_status: ExitError::OutOfGas,
119                    });
120                }
121            }
122            Ok(())
123        };
124        check_cost(cost)?;
125
126        // It's not allowed to call cross contract call precompile in static or delegate mode
127        if is_static {
128            return Err(revert_with_message(consts::ERR_STATIC));
129        } else if context.address != cross_contract_call::ADDRESS.raw() {
130            return Err(revert_with_message(consts::ERR_DELEGATE));
131        }
132
133        let sender = context.caller;
134        let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref())?;
135        let args = CrossContractCallArgs::try_from_slice(input)
136            .map_err(|_| ExitError::Other(Cow::from(consts::ERR_INVALID_INPUT)))?;
137        let (promise, attached_near) = match args {
138            CrossContractCallArgs::Eager(call) => {
139                let call_gas = call.total_gas();
140                let attached_near = call.total_near();
141                let callback_count = call
142                    .promise_count()
143                    .checked_sub(1)
144                    .ok_or_else(|| ExitError::Other(Cow::from(consts::ERR_INVALID_INPUT)))?;
145                let router_exec_cost = costs::ROUTER_EXEC_BASE
146                    + NearGas::new(callback_count * costs::ROUTER_EXEC_PER_CALLBACK.as_u64());
147                let promise = PromiseCreateArgs {
148                    target_account_id,
149                    method: consts::ROUTER_EXEC_NAME.into(),
150                    args: borsh::to_vec(&call)
151                        .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?,
152                    attached_balance: ZERO_YOCTO,
153                    attached_gas: router_exec_cost.saturating_add(call_gas),
154                };
155                (promise, attached_near)
156            }
157            CrossContractCallArgs::Delayed(call) => {
158                let attached_near = call.total_near();
159                let promise = PromiseCreateArgs {
160                    target_account_id,
161                    method: consts::ROUTER_SCHEDULE_NAME.into(),
162                    args: borsh::to_vec(&call)
163                        .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?,
164                    attached_balance: ZERO_YOCTO,
165                    // We don't need to add any gas to the amount need for the schedule call
166                    // since the promise is not executed right away.
167                    attached_gas: costs::ROUTER_SCHEDULE,
168                };
169                (promise, attached_near)
170            }
171        };
172        cost += EthGas::new(promise.attached_gas.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS);
173        check_cost(cost)?;
174
175        let required_near =
176            match state::get_code_version_of_address(&self.io, &Address::new(sender)) {
177                // If there is no deployed version of the router contract then we need to charge for storage staking
178                None => attached_near + state::STORAGE_AMOUNT,
179                Some(_) => attached_near,
180            };
181        // if some NEAR payment is needed, transfer it from the caller to the engine's implicit address
182        if required_near != ZERO_YOCTO {
183            let engine_implicit_address = aurora_engine_sdk::types::near_account_to_evm_address(
184                self.engine_account_id.as_bytes(),
185            );
186            let tx_data = transfer_from_args(
187                sender.0.into(),
188                engine_implicit_address.raw().0.into(),
189                required_near.as_u128().into(),
190            );
191            let wnear_address = state::get_wnear_address(&self.io);
192            let context = aurora_evm::Context {
193                address: wnear_address.raw(),
194                caller: cross_contract_call::ADDRESS.raw(),
195                apparent_value: U256::zero(),
196            };
197            let (exit_reason, return_value) =
198                handle.call(wnear_address.raw(), None, tx_data, None, false, &context);
199            match exit_reason {
200                // Transfer successful, nothing to do
201                aurora_evm::ExitReason::Succeed(_) => (),
202                aurora_evm::ExitReason::Revert(r) => {
203                    return Err(PrecompileFailure::Revert {
204                        exit_status: r,
205                        output: return_value,
206                    });
207                }
208                aurora_evm::ExitReason::Error(e) => {
209                    return Err(PrecompileFailure::Error { exit_status: e });
210                }
211                aurora_evm::ExitReason::Fatal(f) => {
212                    return Err(PrecompileFailure::Fatal { exit_status: f });
213                }
214            };
215        }
216
217        let topics = vec![
218            cross_contract_call::AMOUNT_TOPIC,
219            H256(aurora_engine_types::types::u256_to_arr(&U256::from(
220                required_near.as_u128(),
221            ))),
222        ];
223
224        let promise_log = Log {
225            address: cross_contract_call::ADDRESS.raw(),
226            topics,
227            data: borsh::to_vec(&promise)
228                .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?,
229        };
230
231        Ok(PrecompileOutput {
232            logs: vec![promise_log],
233            cost,
234            ..Default::default()
235        })
236    }
237}
238
239pub mod state {
240    //! Functions for reading state related to the cross-contract call feature
241
242    use aurora_engine_sdk::error::ReadU32Error;
243    use aurora_engine_sdk::io::{StorageIntermediate, IO};
244    use aurora_engine_types::parameters::xcc::CodeVersion;
245    use aurora_engine_types::storage::{self, KeyPrefix};
246    use aurora_engine_types::types::{Address, Yocto};
247
248    pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE";
249    pub const ERR_MISSING_WNEAR_ADDRESS: &str = "ERR_MISSING_WNEAR_ADDRESS";
250    pub const VERSION_KEY: &[u8] = b"version";
251    pub const WNEAR_KEY: &[u8] = b"wnear";
252    /// Amount of NEAR needed to cover storage for a router contract.
253    pub const STORAGE_AMOUNT: Yocto = Yocto::new(2_000_000_000_000_000_000_000_000);
254
255    /// Get the address of the `wNEAR` ERC-20 contract
256    ///
257    /// # Panics
258    ///
259    /// Panic is ok here because there is no sense to continue with corrupted storage.
260    pub fn get_wnear_address<I: IO>(io: &I) -> Address {
261        let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, WNEAR_KEY);
262        io.read_storage(&key).map_or_else(
263            || panic!("{ERR_MISSING_WNEAR_ADDRESS}"),
264            |bytes| Address::try_from_slice(&bytes.to_vec()).expect(ERR_CORRUPTED_STORAGE),
265        )
266    }
267
268    /// Get the latest router contract version.
269    pub fn get_latest_code_version<I: IO>(io: &I) -> CodeVersion {
270        let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, VERSION_KEY);
271        read_version(io, &key).unwrap_or_default()
272    }
273
274    /// Get the version of the currently deploy router for the given address (if it exists).
275    pub fn get_code_version_of_address<I: IO>(io: &I, address: &Address) -> Option<CodeVersion> {
276        let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, address.as_bytes());
277        read_version(io, &key)
278    }
279
280    /// Private utility method for reading code version from storage.
281    fn read_version<I: IO>(io: &I, key: &[u8]) -> Option<CodeVersion> {
282        match io.read_u32(key) {
283            Ok(value) => Some(CodeVersion(value)),
284            Err(ReadU32Error::MissingValue) => None,
285            Err(ReadU32Error::InvalidU32) => panic!("{}", ERR_CORRUPTED_STORAGE),
286        }
287    }
288}
289
290fn transfer_from_args(from: ethabi::Address, to: ethabi::Address, amount: ethabi::Uint) -> Vec<u8> {
291    let args = ethabi::encode(&[
292        ethabi::Token::Address(from),
293        ethabi::Token::Address(to),
294        ethabi::Token::Uint(amount),
295    ]);
296    [&consts::TRANSFER_FROM_SELECTOR, args.as_slice()].concat()
297}
298
299fn create_target_account_id(
300    sender: H160,
301    engine_account_id: &str,
302) -> Result<AccountId, PrecompileFailure> {
303    let mut buffer = [0; 40];
304    hex::encode_to_slice(sender.as_bytes(), &mut buffer)
305        .map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID))?;
306    let sender_in_hex =
307        str::from_utf8(&buffer).map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID))?;
308
309    AccountId::try_from(format!("{sender_in_hex}.{engine_account_id}"))
310        .map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID))
311}
312
313fn revert_with_message(message: &str) -> PrecompileFailure {
314    PrecompileFailure::Revert {
315        exit_status: aurora_evm::ExitRevert::Reverted,
316        output: message.as_bytes().to_vec(),
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use crate::prelude::sdk::types::near_account_to_evm_address;
323    use crate::xcc::cross_contract_call;
324    use aurora_engine_types::vec;
325    use rand::Rng;
326
327    #[test]
328    fn test_precompile_id() {
329        assert_eq!(
330            cross_contract_call::ADDRESS,
331            near_account_to_evm_address(b"nearCrossContractCall")
332        );
333    }
334
335    #[test]
336    fn test_transfer_from_encoding() {
337        let mut rng = rand::thread_rng();
338
339        let from = rng.gen::<[u8; 20]>().into();
340        let to = rng.gen::<[u8; 20]>().into();
341        let amount = rng.gen::<[u8; 32]>().into();
342
343        #[allow(deprecated)]
344        let transfer_from_function = ethabi::Function {
345            name: "transferFrom".into(),
346            inputs: vec![
347                ethabi::Param {
348                    name: "from".into(),
349                    kind: ethabi::ParamType::Address,
350                    internal_type: None,
351                },
352                ethabi::Param {
353                    name: "to".into(),
354                    kind: ethabi::ParamType::Address,
355                    internal_type: None,
356                },
357                ethabi::Param {
358                    name: "amount".into(),
359                    kind: ethabi::ParamType::Uint(256),
360                    internal_type: None,
361                },
362            ],
363            outputs: vec![ethabi::Param {
364                name: String::new(),
365                kind: ethabi::ParamType::Bool,
366                internal_type: None,
367            }],
368            constant: None,
369            state_mutability: ethabi::StateMutability::NonPayable,
370        };
371
372        let expected_tx_data = transfer_from_function
373            .encode_input(&[
374                ethabi::Token::Address(from),
375                ethabi::Token::Address(to),
376                ethabi::Token::Uint(amount),
377            ])
378            .unwrap();
379
380        assert_eq!(
381            super::transfer_from_args(from, to, amount),
382            expected_tx_data
383        );
384    }
385}