aurora_engine_precompiles/
native.rs

1use aurora_engine_types::{
2    account_id::AccountId,
3    borsh,
4    parameters::{
5        PromiseWithCallbackArgs,
6        connector::{
7            ExitToNearPrecompileCallbackArgs, TransferNearArgs, WithdrawCallArgs,
8            WithdrawSerializeType,
9        },
10    },
11    storage::EthConnectorStorageId,
12    types::NEP141Wei,
13};
14use aurora_evm::backend::Log;
15use aurora_evm::{Context, ExitError};
16
17use super::{EvmPrecompileResult, Precompile};
18use crate::PrecompileOutput;
19use crate::native::events::{ExitToNearLegacy, ExitToNearOmni};
20use crate::prelude::types::EthGas;
21use crate::prelude::{
22    Cow, H256, String, ToString, U256, Vec, format,
23    parameters::{PromiseArgs, PromiseCreateArgs},
24    sdk::io::{IO, StorageIntermediate},
25    storage::{KeyPrefix, bytes_to_key},
26    str,
27    types::{Address, Yocto},
28    vec,
29};
30#[cfg(feature = "error_refund")]
31use crate::prelude::{parameters::connector::RefundCallArgs, types};
32use crate::xcc::state::get_wnear_address;
33
34const ERR_TARGET_TOKEN_NOT_FOUND: &str = "Target token not found";
35const UNWRAP_WNEAR_MSG: &str = "unwrap";
36#[cfg(not(feature = "error_refund"))]
37const MIN_INPUT_SIZE: usize = 3;
38#[cfg(feature = "error_refund")]
39const MIN_INPUT_SIZE: usize = 21;
40const MAX_INPUT_SIZE: usize = 1_024;
41
42mod costs {
43    use crate::prelude::types::{EthGas, NearGas};
44
45    // TODO(#483): Determine the correct amount of gas
46    pub(super) const EXIT_TO_NEAR_GAS: EthGas = EthGas::new(0);
47
48    // TODO(#483): Determine the correct amount of gas
49    pub(super) const EXIT_TO_ETHEREUM_GAS: EthGas = EthGas::new(0);
50
51    /// Value determined experimentally based on tests and mainnet data. Example:
52    /// `https://explorer.mainnet.near.org/transactions/5CD7NrqWpK3H8MAAU4mYEPuuWz9AqR9uJkkZJzw5b8PM#D1b5NVRrAsJKUX2ZGs3poKViu1Rgt4RJZXtTfMgdxH4S`
53    pub(super) const FT_TRANSFER_GAS: NearGas = NearGas::new(10_000_000_000_000);
54
55    pub(super) const FT_TRANSFER_CALL_GAS: NearGas = NearGas::new(70_000_000_000_000);
56
57    /// Value determined experimentally based on tests.
58    pub(super) const EXIT_TO_NEAR_CALLBACK_GAS: NearGas = NearGas::new(10_000_000_000_000);
59
60    // TODO(#332): Determine the correct amount of gas
61    pub(super) const WITHDRAWAL_GAS: NearGas = NearGas::new(100_000_000_000_000);
62}
63
64pub mod events {
65    use crate::prelude::{H256, String, ToString, U256, types::Address, vec};
66
67    /// Derived from event signature (see `tests::test_exit_signatures`)
68    pub const EXIT_TO_NEAR_SIGNATURE: H256 = crate::make_h256(
69        0x5a91b8bc9c1981673db8fb226dbd8fcd,
70        0xd0c23f45cd28abb31403a5392f6dd0c7,
71    );
72    /// Derived from event signature (see `tests::test_exit_signatures`)
73    pub const EXIT_TO_ETH_SIGNATURE: H256 = crate::make_h256(
74        0xd046c2bb01a5622bc4b9696332391d87,
75        0x491373762eeac0831c48400e2d5a5f07,
76    );
77
78    /// The exit precompile events have an `erc20_address` field to indicate
79    /// which ERC-20 token is being withdrawn. However, ETH is not an ERC-20 token
80    /// So we need to have some other address to fill this field. This constant is
81    /// used for this purpose.
82    pub const ETH_ADDRESS: Address = Address::zero();
83
84    /// `ExitToNear`(
85    ///    Address indexed sender,
86    ///    Address indexed `erc20_address`,
87    ///    string indexed dest,
88    ///    uint amount
89    /// )
90    /// Note: in the ERC-20 exit case `sender` == `erc20_address` because it is
91    /// the ERC-20 contract which calls the exit precompile. However, in the case
92    /// of ETH exit the sender will give the true sender (and the `erc20_address`
93    /// will not be meaningful because ETH is not an ERC-20 token).
94    pub enum ExitToNear {
95        Legacy(ExitToNearLegacy),
96        Omni(ExitToNearOmni),
97    }
98
99    impl ExitToNear {
100        #[must_use]
101        pub fn encode(self) -> ethabi::RawLog {
102            match self {
103                Self::Legacy(legacy) => legacy.encode(),
104                Self::Omni(omni) => omni.encode(),
105            }
106        }
107    }
108
109    pub struct ExitToNearLegacy {
110        pub sender: Address,
111        pub erc20_address: Address,
112        pub dest: String,
113        pub amount: U256,
114    }
115
116    impl ExitToNearLegacy {
117        #[must_use]
118        pub fn encode(self) -> ethabi::RawLog {
119            let data = ethabi::encode(&[ethabi::Token::Uint(self.amount.to_big_endian().into())]);
120            let topics = vec![
121                EXIT_TO_NEAR_SIGNATURE.0.into(),
122                encode_address(self.sender),
123                encode_address(self.erc20_address),
124                aurora_engine_sdk::keccak(&ethabi::encode(&[ethabi::Token::String(self.dest)]))
125                    .0
126                    .into(),
127            ];
128
129            ethabi::RawLog { topics, data }
130        }
131    }
132
133    pub struct ExitToNearOmni {
134        pub sender: Address,
135        pub erc20_address: Address,
136        pub dest: String,
137        pub amount: U256,
138        pub msg: String,
139    }
140
141    impl ExitToNearOmni {
142        #[must_use]
143        pub fn encode(self) -> ethabi::RawLog {
144            let data = ethabi::encode(&[
145                ethabi::Token::Uint(self.amount.to_big_endian().into()),
146                ethabi::Token::String(self.msg),
147            ]);
148            let topics = vec![
149                EXIT_TO_NEAR_SIGNATURE.0.into(),
150                encode_address(self.sender),
151                encode_address(self.erc20_address),
152                aurora_engine_sdk::keccak(&ethabi::encode(&[ethabi::Token::String(self.dest)]))
153                    .0
154                    .into(),
155            ];
156
157            ethabi::RawLog { topics, data }
158        }
159    }
160
161    #[must_use]
162    pub fn exit_to_near_schema() -> ethabi::Event {
163        ethabi::Event {
164            name: "ExitToNear".to_string(),
165            inputs: vec![
166                ethabi::EventParam {
167                    name: "sender".to_string(),
168                    kind: ethabi::ParamType::Address,
169                    indexed: true,
170                },
171                ethabi::EventParam {
172                    name: "erc20_address".to_string(),
173                    kind: ethabi::ParamType::Address,
174                    indexed: true,
175                },
176                ethabi::EventParam {
177                    name: "dest".to_string(),
178                    kind: ethabi::ParamType::String,
179                    indexed: true,
180                },
181                ethabi::EventParam {
182                    name: "amount".to_string(),
183                    kind: ethabi::ParamType::Uint(256),
184                    indexed: false,
185                },
186            ],
187            anonymous: false,
188        }
189    }
190
191    /// `ExitToEth`(
192    ///    Address indexed sender,
193    ///    Address indexed `erc20_address`,
194    ///    string indexed dest,
195    ///    uint amount
196    /// )
197    /// Note: in the ERC-20 exit case `sender` == `erc20_address` because it is
198    /// the ERC-20 contract which calls the exit precompile. However, in the case
199    /// of ETH exit the sender will give the true sender (and the `erc20_address`
200    /// will not be meaningful because ETH is not an ERC-20 token).
201    pub struct ExitToEth {
202        pub sender: Address,
203        pub erc20_address: Address,
204        pub dest: Address,
205        pub amount: U256,
206    }
207
208    impl ExitToEth {
209        #[must_use]
210        pub fn encode(self) -> ethabi::RawLog {
211            let data = ethabi::encode(&[ethabi::Token::Uint(self.amount.to_big_endian().into())]);
212            let topics = vec![
213                EXIT_TO_ETH_SIGNATURE.0.into(),
214                encode_address(self.sender),
215                encode_address(self.erc20_address),
216                encode_address(self.dest),
217            ];
218
219            ethabi::RawLog { topics, data }
220        }
221    }
222
223    #[must_use]
224    pub fn exit_to_eth_schema() -> ethabi::Event {
225        ethabi::Event {
226            name: "ExitToEth".to_string(),
227            inputs: vec![
228                ethabi::EventParam {
229                    name: "sender".to_string(),
230                    kind: ethabi::ParamType::Address,
231                    indexed: true,
232                },
233                ethabi::EventParam {
234                    name: "erc20_address".to_string(),
235                    kind: ethabi::ParamType::Address,
236                    indexed: true,
237                },
238                ethabi::EventParam {
239                    name: "dest".to_string(),
240                    kind: ethabi::ParamType::Address,
241                    indexed: true,
242                },
243                ethabi::EventParam {
244                    name: "amount".to_string(),
245                    kind: ethabi::ParamType::Uint(256),
246                    indexed: false,
247                },
248            ],
249            anonymous: false,
250        }
251    }
252
253    fn encode_address(a: Address) -> ethabi::Hash {
254        let mut result = [0u8; 32];
255        result[12..].copy_from_slice(a.as_bytes());
256        result.into()
257    }
258}
259
260trait EthConnector {
261    fn get_eth_connector_contract_account(&self) -> Result<AccountId, ExitError>;
262}
263
264/// Transfer ETH(base) or ERC-20 tokens to NEAR.
265pub struct ExitToNear<I> {
266    current_account_id: AccountId,
267    io: I,
268}
269
270pub mod exit_to_near {
271    use crate::prelude::types::{Address, make_address};
272
273    /// Exit to NEAR precompile address
274    ///
275    /// Address: `0xe9217bc70b7ed1f598ddd3199e80b093fa71124f`
276    /// This address is computed as: `&keccak("exitToNear")[12..]`
277    pub const ADDRESS: Address = make_address(0xe9217bc7, 0x0b7ed1f598ddd3199e80b093fa71124f);
278}
279
280impl<I> ExitToNear<I> {
281    pub const fn new(current_account_id: AccountId, io: I) -> Self {
282        Self {
283            current_account_id,
284            io,
285        }
286    }
287}
288
289impl<I: IO> EthConnector for ExitToNear<I> {
290    fn get_eth_connector_contract_account(&self) -> Result<AccountId, ExitError> {
291        get_eth_connector_contract_account(&self.io)
292    }
293}
294
295fn validate_input_size(input: &[u8], min: usize, max: usize) -> Result<(), ExitError> {
296    if input.len() < min || input.len() > max {
297        return Err(ExitError::Other(Cow::from("ERR_INVALID_INPUT")));
298    }
299    Ok(())
300}
301
302fn get_nep141_from_erc20<I: IO>(erc20_token: &[u8], io: &I) -> Result<AccountId, ExitError> {
303    AccountId::try_from(
304        io.read_storage(bytes_to_key(KeyPrefix::Erc20Nep141Map, erc20_token).as_slice())
305            .map(|s| s.to_vec())
306            .ok_or(ExitError::Other(Cow::Borrowed(ERR_TARGET_TOKEN_NOT_FOUND)))?,
307    )
308    .map_err(|_| ExitError::Other(Cow::Borrowed("ERR_INVALID_NEP141_ACCOUNT")))
309}
310
311fn get_eth_connector_contract_account<I: IO>(io: &I) -> Result<AccountId, ExitError> {
312    io.read_storage(&construct_contract_key(
313        EthConnectorStorageId::EthConnectorAccount,
314    ))
315    .ok_or(ExitError::Other(Cow::Borrowed("ERR_KEY_NOT_FOUND")))
316    .and_then(|x| {
317        x.to_value()
318            .map_err(|_| ExitError::Other(Cow::Borrowed("ERR_DESERIALIZE")))
319    })
320}
321
322fn get_withdraw_serialize_type<I: IO>(io: &I) -> Result<WithdrawSerializeType, ExitError> {
323    io.read_storage(&construct_contract_key(
324        EthConnectorStorageId::WithdrawSerializationType,
325    ))
326    .map_or(Ok(WithdrawSerializeType::Borsh), |value| {
327        value
328            .to_value()
329            .map_err(|_| ExitError::Other(Cow::Borrowed("ERR_DESERIALIZE")))
330    })
331}
332
333fn construct_contract_key(suffix: EthConnectorStorageId) -> Vec<u8> {
334    bytes_to_key(KeyPrefix::EthConnector, &[u8::from(suffix)])
335}
336
337fn parse_amount(input: &[u8]) -> Result<U256, ExitError> {
338    let amount = U256::from_big_endian(input);
339
340    if amount > U256::from(u128::MAX) {
341        return Err(ExitError::Other(Cow::from("ERR_INVALID_AMOUNT")));
342    }
343
344    Ok(amount)
345}
346
347#[cfg_attr(test, derive(Debug, PartialEq))]
348struct Recipient<'a> {
349    receiver_account_id: AccountId,
350    message: Option<Message<'a>>,
351}
352
353#[cfg_attr(test, derive(Debug, PartialEq))]
354enum Message<'a> {
355    UnwrapWnear,
356    Omni(&'a str),
357}
358
359fn parse_recipient(recipient: &[u8]) -> Result<Recipient<'_>, ExitError> {
360    let recipient = str::from_utf8(recipient)
361        .map_err(|_| ExitError::Other(Cow::from("ERR_INVALID_RECEIVER_ACCOUNT_ID")))?;
362    let (receiver_account_id, message) = recipient.split_once(':').map_or_else(
363        || (recipient, None),
364        |(recipient, msg)| {
365            if msg == UNWRAP_WNEAR_MSG {
366                (recipient, Some(Message::UnwrapWnear))
367            } else {
368                (recipient, Some(Message::Omni(msg)))
369            }
370        },
371    );
372
373    Ok(Recipient {
374        receiver_account_id: receiver_account_id
375            .parse()
376            .map_err(|_| ExitError::Other(Cow::from("ERR_INVALID_RECEIVER_ACCOUNT_ID")))?,
377        message,
378    })
379}
380
381impl<I: IO> Precompile for ExitToNear<I> {
382    fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError> {
383        Ok(costs::EXIT_TO_NEAR_GAS)
384    }
385
386    #[allow(clippy::too_many_lines)]
387    fn run(
388        &self,
389        input: &[u8],
390        target_gas: Option<EthGas>,
391        context: &Context,
392        is_static: bool,
393    ) -> EvmPrecompileResult {
394        // ETH (base) transfer input format: (85 bytes)
395        //  - flag (1 byte)
396        //  - refund_address (20 bytes), present if the feature "error_refund" is enabled
397        //  - recipient_account_id (max MAX_INPUT_SIZE - 20 - 1 bytes)
398        // ERC-20 transfer input format: (124 bytes)
399        //  - flag (1 byte)
400        //  - refund_address (20 bytes), present if the feature "error_refund" is enabled.
401        //  - amount (32 bytes)
402        //  - recipient_account_id (max MAX_INPUT_SIZE - 1 - (20) - 32 bytes)
403        //  - `:unwrap` suffix in a case of wNEAR (7 bytes)
404        if let Some(target_gas) = target_gas {
405            if Self::required_gas(input)? > target_gas {
406                return Err(ExitError::OutOfGas);
407            }
408        }
409
410        // It's not allowed to call exit precompiles in static mode
411        if is_static {
412            return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_STATIC")));
413        } else if context.address != exit_to_near::ADDRESS.raw() {
414            return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_DELEGATE")));
415        }
416
417        let exit_to_near_params = ExitToNearParams::try_from(input)?;
418
419        let (nep141_address, args, exit_event, method, transfer_near_args) =
420            match exit_to_near_params {
421                // ETH(base) token transfer
422                //
423                // Input slice format:
424                //  recipient_account_id (bytes) - the NEAR recipient account which will receive
425                //  NEP-141 (base) tokens, or also can contain the `:unwrap` suffix in case of
426                //  withdrawing wNEAR, or other message of JSON in case of OMNI, or address of
427                //  receiver in case of transfer tokens to another engine contract.
428                ExitToNearParams::BaseToken(ref exit_params) => {
429                    let eth_connector_account_id = self.get_eth_connector_contract_account()?;
430                    exit_base_token_to_near(eth_connector_account_id, context, exit_params)?
431                }
432                // ERC-20 token transfer
433                //
434                // This precompile branch is expected to be called from the ERC-20 burn function.
435                //
436                // Input slice format:
437                //  amount (U256 big-endian bytes) - the amount that was burned
438                //  recipient_account_id (bytes) - the NEAR recipient account which will receive
439                //  NEP-141 tokens, or also can contain the `:unwrap` suffix in case of withdrawing
440                //  wNEAR, or other message of JSON in case of OMNI, or address of receiver in case
441                //  of transfer tokens to another engine contract.
442                ExitToNearParams::Erc20TokenParams(ref exit_params) => {
443                    exit_erc20_token_to_near(context, exit_params, &self.io)?
444                }
445            };
446
447        let callback_args = ExitToNearPrecompileCallbackArgs {
448            #[cfg(feature = "error_refund")]
449            refund: refund_call_args(&exit_to_near_params, &exit_event),
450            #[cfg(not(feature = "error_refund"))]
451            refund: None,
452            transfer_near: transfer_near_args,
453        };
454        let attached_gas = if method == "ft_transfer_call" {
455            costs::FT_TRANSFER_CALL_GAS
456        } else {
457            costs::FT_TRANSFER_GAS
458        };
459
460        let transfer_promise = PromiseCreateArgs {
461            target_account_id: nep141_address,
462            method,
463            args: args.into_bytes(),
464            attached_balance: Yocto::new(1),
465            attached_gas,
466        };
467
468        let promise = if callback_args == ExitToNearPrecompileCallbackArgs::default() {
469            PromiseArgs::Create(transfer_promise)
470        } else {
471            PromiseArgs::Callback(PromiseWithCallbackArgs {
472                base: transfer_promise,
473                callback: PromiseCreateArgs {
474                    target_account_id: self.current_account_id.clone(),
475                    method: "exit_to_near_precompile_callback".to_string(),
476                    args: borsh::to_vec(&callback_args).unwrap(),
477                    attached_balance: Yocto::new(0),
478                    attached_gas: costs::EXIT_TO_NEAR_CALLBACK_GAS,
479                },
480            })
481        };
482        let promise_log = Log {
483            address: exit_to_near::ADDRESS.raw(),
484            topics: Vec::new(),
485            data: borsh::to_vec(&promise).unwrap(),
486        };
487        let ethabi::RawLog { topics, data } = exit_event.encode();
488        let exit_event_log = Log {
489            address: exit_to_near::ADDRESS.raw(),
490            topics: topics.into_iter().map(|h| H256::from(h.0)).collect(),
491            data,
492        };
493
494        Ok(PrecompileOutput {
495            logs: vec![promise_log, exit_event_log],
496            cost: Self::required_gas(input)?,
497            output: Vec::new(),
498        })
499    }
500}
501
502fn exit_base_token_to_near(
503    eth_connector_account_id: AccountId,
504    context: &Context,
505    exit_params: &BaseTokenParams,
506) -> Result<
507    (
508        AccountId,
509        String,
510        events::ExitToNear,
511        String,
512        Option<TransferNearArgs>,
513    ),
514    ExitError,
515> {
516    match exit_params.message {
517        Some(Message::Omni(msg)) => Ok((
518            eth_connector_account_id,
519            ft_transfer_call_args(
520                &exit_params.receiver_account_id,
521                context.apparent_value,
522                msg,
523            )?,
524            events::ExitToNear::Omni(ExitToNearOmni {
525                sender: Address::new(context.caller),
526                erc20_address: events::ETH_ADDRESS,
527                dest: exit_params.receiver_account_id.to_string(),
528                amount: context.apparent_value,
529                msg: msg.to_string(),
530            }),
531            "ft_transfer_call".to_string(),
532            None,
533        )),
534        None => Ok((
535            eth_connector_account_id,
536            // There is no way to inject json, given the encoding of both arguments
537            // as decimal and valid account id respectively.
538            format!(
539                r#"{{"receiver_id":"{}","amount":"{}"}}"#,
540                exit_params.receiver_account_id,
541                context.apparent_value.as_u128()
542            ),
543            events::ExitToNear::Legacy(ExitToNearLegacy {
544                sender: Address::new(context.caller),
545                erc20_address: events::ETH_ADDRESS,
546                dest: exit_params.receiver_account_id.to_string(),
547                amount: context.apparent_value,
548            }),
549            "ft_transfer".to_string(),
550            None,
551        )),
552        _ => Err(ExitError::Other(Cow::from("ERR_INVALID_MESSAGE"))),
553    }
554}
555
556fn exit_erc20_token_to_near<I: IO>(
557    context: &Context,
558    exit_params: &Erc20TokenParams,
559    io: &I,
560) -> Result<
561    (
562        AccountId,
563        String,
564        events::ExitToNear,
565        String,
566        Option<TransferNearArgs>,
567    ),
568    ExitError,
569> {
570    // In case of withdrawing ERC-20 tokens, the `apparent_value` should be zero. In opposite way
571    // the funds will be locked in the address of the precompile without any possibility
572    // to withdraw them in the future. So, in case if the `apparent_value` is not zero, the error
573    // will be returned to prevent that.
574    if context.apparent_value != U256::zero() {
575        return Err(ExitError::Other(Cow::from(
576            "ERR_ETH_ATTACHED_FOR_ERC20_EXIT",
577        )));
578    }
579
580    let erc20_address = context.caller; // because ERC-20 contract calls the precompile.
581    let nep141_account_id = get_nep141_from_erc20(erc20_address.as_bytes(), io)?;
582
583    let (nep141_account_id, args, method, transfer_near_args, event) = match exit_params.message {
584        // wNEAR address should be set via the `factory_set_wnear_address` transaction first.
585        Some(Message::UnwrapWnear) if erc20_address == get_wnear_address(io).raw() =>
586        // The flow is following here:
587        // 1. We call `near_withdraw` on wNEAR account id on `aurora` behalf.
588        // In such way we unwrap wNEAR to NEAR.
589        // 2. After that, we call callback `exit_to_near_precompile_callback` on the `aurora`
590        // in which make transfer of unwrapped NEAR to the `target_account_id`.
591        {
592            (
593                nep141_account_id,
594                format!(r#"{{"amount":"{}"}}"#, exit_params.amount.as_u128()),
595                "near_withdraw",
596                Some(TransferNearArgs {
597                    target_account_id: exit_params.receiver_account_id.clone(),
598                    amount: exit_params.amount.as_u128(),
599                }),
600                events::ExitToNear::Legacy(ExitToNearLegacy {
601                    sender: Address::new(erc20_address),
602                    erc20_address: Address::new(erc20_address),
603                    dest: exit_params.receiver_account_id.to_string(),
604                    amount: exit_params.amount,
605                }),
606            )
607        }
608        // In this flow, we're just forwarding the `msg` to the `ft_transfer_call` transaction.
609        Some(Message::Omni(msg)) => (
610            nep141_account_id,
611            ft_transfer_call_args(&exit_params.receiver_account_id, exit_params.amount, msg)?,
612            "ft_transfer_call",
613            None,
614            events::ExitToNear::Omni(ExitToNearOmni {
615                sender: Address::new(erc20_address),
616                erc20_address: Address::new(erc20_address),
617                dest: exit_params.receiver_account_id.to_string(),
618                amount: exit_params.amount,
619                msg: msg.to_string(),
620            }),
621        ),
622        // The legacy flow. Just withdraw the tokens to the NEAR account id.
623        // P.S. We use underscore here instead of `None` to handle the case when a user
624        // could add the `unwrap` suffix for non wNEAR ERC-20 token by mistake.
625        _ => {
626            // There is no way to inject json, given the encoding of both arguments
627            // as decimal and valid account id respectively.
628            (
629                nep141_account_id,
630                format!(
631                    r#"{{"receiver_id":"{}","amount":"{}"}}"#,
632                    exit_params.receiver_account_id,
633                    exit_params.amount.as_u128()
634                ),
635                "ft_transfer",
636                None,
637                events::ExitToNear::Legacy(ExitToNearLegacy {
638                    sender: Address::new(erc20_address),
639                    erc20_address: Address::new(erc20_address),
640                    dest: exit_params.receiver_account_id.to_string(),
641                    amount: exit_params.amount,
642                }),
643            )
644        }
645    };
646
647    Ok((
648        nep141_account_id,
649        args,
650        event,
651        method.to_string(),
652        transfer_near_args,
653    ))
654}
655
656#[allow(clippy::unnecessary_wraps)]
657fn json_args(address: Address, amount: U256) -> Result<Vec<u8>, ExitError> {
658    Ok(format!(
659        r#"{{"amount":"{}","recipient":"{}"}}"#,
660        amount.as_u128(),
661        address.encode(),
662    )
663    .into_bytes())
664}
665
666fn borsh_args(address: Address, amount: U256) -> Result<Vec<u8>, ExitError> {
667    borsh::to_vec(&WithdrawCallArgs {
668        recipient_address: address,
669        amount: NEP141Wei::new(amount.as_u128()),
670    })
671    .map_err(|_| ExitError::Other(Cow::from("ERR_BORSH_SERIALIZE")))
672}
673
674#[cfg_attr(test, derive(Debug, PartialEq))]
675enum ExitToNearParams<'a> {
676    BaseToken(BaseTokenParams<'a>),
677    Erc20TokenParams(Erc20TokenParams<'a>),
678}
679
680#[cfg_attr(test, derive(Debug, PartialEq))]
681struct BaseTokenParams<'a> {
682    #[cfg(feature = "error_refund")]
683    refund_address: Address,
684    receiver_account_id: AccountId,
685    message: Option<Message<'a>>,
686}
687
688#[cfg_attr(test, derive(Debug, PartialEq))]
689struct Erc20TokenParams<'a> {
690    #[cfg(feature = "error_refund")]
691    refund_address: Address,
692    receiver_account_id: AccountId,
693    amount: U256,
694    message: Option<Message<'a>>,
695}
696
697#[cfg(feature = "error_refund")]
698#[allow(clippy::unnecessary_wraps)]
699fn refund_call_args(
700    params: &ExitToNearParams,
701    event: &events::ExitToNear,
702) -> Option<RefundCallArgs> {
703    Some(RefundCallArgs {
704        recipient_address: match params {
705            ExitToNearParams::BaseToken(params) => params.refund_address,
706            ExitToNearParams::Erc20TokenParams(params) => params.refund_address,
707        },
708        erc20_address: match params {
709            ExitToNearParams::BaseToken(_) => None,
710            ExitToNearParams::Erc20TokenParams(_) => {
711                let erc20_address = match event {
712                    events::ExitToNear::Legacy(legacy) => legacy.erc20_address,
713                    events::ExitToNear::Omni(omni) => omni.erc20_address,
714                };
715                Some(erc20_address)
716            }
717        },
718        amount: types::u256_to_arr(&match event {
719            events::ExitToNear::Legacy(legacy) => legacy.amount,
720            events::ExitToNear::Omni(omni) => omni.amount,
721        }),
722    })
723}
724
725impl<'a> TryFrom<&'a [u8]> for ExitToNearParams<'a> {
726    type Error = ExitError;
727
728    fn try_from(input: &'a [u8]) -> Result<Self, Self::Error> {
729        // The first byte of the input is a flag, selecting the behavior to be triggered:
730        // 0x00 -> Eth(base) token withdrawal
731        // 0x01 -> ERC-20 token withdrawal
732        let flag = input
733            .first()
734            .copied()
735            .ok_or_else(|| ExitError::Other(Cow::from("ERR_MISSING_FLAG")))?;
736
737        #[cfg(feature = "error_refund")]
738        let (refund_address, input) = parse_input(input)?;
739        #[cfg(not(feature = "error_refund"))]
740        let input = parse_input(input)?;
741
742        match flag {
743            0x0 => {
744                let Recipient {
745                    receiver_account_id,
746                    message,
747                } = parse_recipient(input)?;
748
749                Ok(Self::BaseToken(BaseTokenParams {
750                    #[cfg(feature = "error_refund")]
751                    refund_address,
752                    receiver_account_id,
753                    message,
754                }))
755            }
756            0x1 => {
757                let amount = parse_amount(&input[..32])?;
758                let Recipient {
759                    receiver_account_id,
760                    message,
761                } = parse_recipient(&input[32..])?;
762
763                Ok(Self::Erc20TokenParams(Erc20TokenParams {
764                    #[cfg(feature = "error_refund")]
765                    refund_address,
766                    receiver_account_id,
767                    amount,
768                    message,
769                }))
770            }
771            _ => Err(ExitError::Other(Cow::from("ERR_INVALID_FLAG"))),
772        }
773    }
774}
775
776#[cfg(feature = "error_refund")]
777fn parse_input(input: &[u8]) -> Result<(Address, &[u8]), ExitError> {
778    validate_input_size(input, MIN_INPUT_SIZE, MAX_INPUT_SIZE)?;
779    let mut buffer = [0; 20];
780    buffer.copy_from_slice(&input[1..21]);
781    let refund_address = Address::from_array(buffer);
782    Ok((refund_address, &input[21..]))
783}
784
785#[cfg(not(feature = "error_refund"))]
786fn parse_input(input: &[u8]) -> Result<&[u8], ExitError> {
787    validate_input_size(input, MIN_INPUT_SIZE, MAX_INPUT_SIZE)?;
788    Ok(&input[1..])
789}
790
791#[derive(serde::Serialize)]
792struct FtTransferCallArgs<'a> {
793    receiver_id: &'a AccountId,
794    amount: String,
795    msg: &'a str,
796}
797
798fn ft_transfer_call_args(
799    receiver_id: &AccountId,
800    amount: U256,
801    msg: &str,
802) -> Result<String, ExitError> {
803    if amount > U256::from(u128::MAX) {
804        return Err(ExitError::Other(Cow::from("ERR_INVALID_AMOUNT")));
805    }
806
807    serde_json::to_string(&FtTransferCallArgs {
808        receiver_id,
809        amount: amount.to_string(),
810        msg,
811    })
812    .map_err(|_| ExitError::Other(Cow::from("ERR_SERIALIZE_JSON")))
813}
814
815pub struct ExitToEthereum<I> {
816    io: I,
817}
818
819pub mod exit_to_ethereum {
820    use crate::prelude::types::{Address, make_address};
821
822    /// Exit to Ethereum precompile address
823    ///
824    /// Address: `0xb0bd02f6a392af548bdf1cfaee5dfa0eefcc8eab`
825    /// This address is computed as: `&keccak("exitToEthereum")[12..]`
826    pub const ADDRESS: Address = make_address(0xb0bd02f6, 0xa392af548bdf1cfaee5dfa0eefcc8eab);
827}
828
829impl<I> ExitToEthereum<I> {
830    pub const fn new(io: I) -> Self {
831        Self { io }
832    }
833}
834
835impl<I: IO> EthConnector for ExitToEthereum<I> {
836    fn get_eth_connector_contract_account(&self) -> Result<AccountId, ExitError> {
837        let eth_connector_account_id = get_eth_connector_contract_account(&self.io)?;
838        Ok(eth_connector_account_id)
839    }
840}
841
842impl<I: IO> Precompile for ExitToEthereum<I> {
843    fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError> {
844        Ok(costs::EXIT_TO_ETHEREUM_GAS)
845    }
846
847    #[allow(clippy::too_many_lines)]
848    fn run(
849        &self,
850        input: &[u8],
851        target_gas: Option<EthGas>,
852        context: &Context,
853        is_static: bool,
854    ) -> EvmPrecompileResult {
855        // ETH (Base token) transfer input format (min size 21 bytes)
856        //  - flag (1 byte)
857        //  - eth_recipient (20 bytes)
858        // ERC-20 transfer input format: max 53 bytes
859        //  - flag (1 byte)
860        //  - amount (32 bytes)
861        //  - eth_recipient (20 bytes)
862        validate_input_size(input, 21, 53)?;
863        if let Some(target_gas) = target_gas {
864            if Self::required_gas(input)? > target_gas {
865                return Err(ExitError::OutOfGas);
866            }
867        }
868
869        // It's not allowed to call exit precompiles in static mode
870        if is_static {
871            return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_STATIC")));
872        } else if context.address != exit_to_ethereum::ADDRESS.raw() {
873            return Err(ExitError::Other(Cow::from("ERR_INVALID_IN_DELEGATE")));
874        }
875
876        // First byte of the input is a flag, selecting the behavior to be triggered:
877        //  0x00 -> ETH (Base token) token transfer
878        //  0x01 -> ERC-20 transfer
879        let mut input = input;
880        let flag = input[0];
881        input = &input[1..];
882
883        let (nep141_address, serialized_args, exit_event) = match flag {
884            0x0 => {
885                // ETH (base) transfer
886                //
887                // Input slice format:
888                //  eth_recipient (20 bytes) - the address of recipient which will receive ETH on Ethereum
889                let recipient_address: Address = input
890                    .try_into()
891                    .map_err(|_| ExitError::Other(Cow::from("ERR_INVALID_RECIPIENT_ADDRESS")))?;
892                let serialize_fn = match get_withdraw_serialize_type(&self.io)? {
893                    WithdrawSerializeType::Json => json_args,
894                    WithdrawSerializeType::Borsh => borsh_args,
895                };
896                let eth_connector_account_id = self.get_eth_connector_contract_account()?;
897
898                (
899                    eth_connector_account_id,
900                    // There is no way to inject json, given the encoding of both arguments
901                    // as decimal and hexadecimal respectively.
902                    serialize_fn(recipient_address, context.apparent_value)?,
903                    events::ExitToEth {
904                        sender: Address::new(context.caller),
905                        erc20_address: events::ETH_ADDRESS,
906                        dest: recipient_address,
907                        amount: context.apparent_value,
908                    },
909                )
910            }
911            0x1 => {
912                // ERC-20 transfer
913                //
914                // This precompile branch is expected to be called from the ERC20 withdraw function
915                // (or burn function with some flag provided that this is expected to be withdrawn)
916                //
917                // Input slice format:
918                //  amount (U256 big-endian bytes) - the amount that was burned
919                //  eth_recipient (20 bytes) - the address of recipient which will receive ETH on Ethereum
920
921                if context.apparent_value != U256::from(0) {
922                    return Err(ExitError::Other(Cow::from(
923                        "ERR_ETH_ATTACHED_FOR_ERC20_EXIT",
924                    )));
925                }
926
927                let erc20_address = context.caller;
928                let nep141_address = get_nep141_from_erc20(erc20_address.as_bytes(), &self.io)?;
929                let amount = parse_amount(&input[..32])?;
930
931                input = &input[32..];
932
933                if input.len() == 20 {
934                    // Parse ethereum address in hex
935                    let mut buffer = [0; 40];
936                    hex::encode_to_slice(input, &mut buffer).unwrap();
937                    let recipient_in_hex = str::from_utf8(&buffer).map_err(|_| {
938                        ExitError::Other(Cow::from("ERR_INVALID_RECIPIENT_ADDRESS"))
939                    })?;
940                    // unwrap cannot fail since we checked the length already
941                    let recipient_address = Address::try_from_slice(input).map_err(|_| {
942                        ExitError::Other(crate::prelude::Cow::from("ERR_WRONG_ADDRESS"))
943                    })?;
944
945                    (
946                        nep141_address,
947                        // There is no way to inject json, given the encoding of both arguments
948                        // as decimal and hexadecimal respectively.
949                        format!(
950                            r#"{{"amount": "{}", "recipient": "{}"}}"#,
951                            amount.as_u128(),
952                            recipient_in_hex
953                        )
954                        .into_bytes(),
955                        events::ExitToEth {
956                            sender: Address::new(erc20_address),
957                            erc20_address: Address::new(erc20_address),
958                            dest: recipient_address,
959                            amount,
960                        },
961                    )
962                } else {
963                    return Err(ExitError::Other(Cow::from("ERR_INVALID_RECIPIENT_ADDRESS")));
964                }
965            }
966            _ => {
967                return Err(ExitError::Other(Cow::from(
968                    "ERR_INVALID_RECEIVER_ACCOUNT_ID",
969                )));
970            }
971        };
972
973        let withdraw_promise = PromiseCreateArgs {
974            target_account_id: nep141_address,
975            method: "withdraw".to_string(),
976            args: serialized_args,
977            attached_balance: Yocto::new(1),
978            attached_gas: costs::WITHDRAWAL_GAS,
979        };
980
981        let promise = borsh::to_vec(&PromiseArgs::Create(withdraw_promise)).unwrap();
982        let promise_log = Log {
983            address: exit_to_ethereum::ADDRESS.raw(),
984            topics: Vec::new(),
985            data: promise,
986        };
987        let ethabi::RawLog { topics, data } = exit_event.encode();
988        let exit_event_log = Log {
989            address: exit_to_ethereum::ADDRESS.raw(),
990            topics: topics.into_iter().map(|h| H256::from(h.0)).collect(),
991            data,
992        };
993
994        Ok(PrecompileOutput {
995            logs: vec![promise_log, exit_event_log],
996            cost: Self::required_gas(input)?,
997            output: Vec::new(),
998        })
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::{
1005        BaseTokenParams, Erc20TokenParams, ExitToNearParams, Message, exit_to_ethereum,
1006        exit_to_near, parse_amount, parse_input, parse_recipient, validate_input_size,
1007    };
1008    use crate::{native::Recipient, prelude::sdk::types::near_account_to_evm_address};
1009    use aurora_engine_types::U256;
1010    #[cfg(feature = "error_refund")]
1011    use aurora_engine_types::types::Address;
1012
1013    #[test]
1014    fn test_precompile_id() {
1015        assert_eq!(
1016            exit_to_ethereum::ADDRESS,
1017            near_account_to_evm_address(b"exitToEthereum")
1018        );
1019        assert_eq!(
1020            exit_to_near::ADDRESS,
1021            near_account_to_evm_address(b"exitToNear")
1022        );
1023    }
1024
1025    #[test]
1026    fn test_exit_signatures() {
1027        let exit_to_near = super::events::exit_to_near_schema();
1028        let exit_to_eth = super::events::exit_to_eth_schema();
1029
1030        assert_eq!(
1031            exit_to_near.signature().0,
1032            super::events::EXIT_TO_NEAR_SIGNATURE.0
1033        );
1034        assert_eq!(
1035            exit_to_eth.signature().0,
1036            super::events::EXIT_TO_ETH_SIGNATURE.0
1037        );
1038    }
1039
1040    #[test]
1041    fn test_check_invalid_input_lt_min() {
1042        let input = [0u8; 4];
1043        assert!(validate_input_size(&input, 10, 20).is_err());
1044        assert!(validate_input_size(&input, 5, 0).is_err());
1045    }
1046
1047    #[test]
1048    fn test_check_invalid_max_value_for_input() {
1049        let input = [0u8; 4];
1050        assert!(validate_input_size(&input, 5, 0).is_err());
1051    }
1052
1053    #[test]
1054    fn test_check_invalid_input_gt_max() {
1055        let input = [1u8; 55];
1056        assert!(validate_input_size(&input, 10, 54).is_err());
1057    }
1058
1059    #[test]
1060    fn test_check_valid_input() {
1061        let input = [1u8; 55];
1062        validate_input_size(&input, 10, input.len()).unwrap();
1063        validate_input_size(&input, 0, input.len()).unwrap();
1064    }
1065
1066    #[test]
1067    #[should_panic(expected = "ERR_INVALID_AMOUNT")]
1068    fn test_exit_with_invalid_amount() {
1069        let input = (U256::from(u128::MAX) + 1).to_big_endian();
1070        parse_amount(input.as_slice()).unwrap();
1071    }
1072
1073    #[test]
1074    fn test_exit_with_valid_amount() {
1075        let input = U256::from(u128::MAX).to_big_endian();
1076        assert_eq!(
1077            parse_amount(input.as_slice()).unwrap(),
1078            U256::from(u128::MAX)
1079        );
1080    }
1081
1082    #[test]
1083    fn test_parse_recipient() {
1084        assert_eq!(
1085            parse_recipient(b"test.near").unwrap(),
1086            Recipient {
1087                receiver_account_id: "test.near".parse().unwrap(),
1088                message: None,
1089            }
1090        );
1091
1092        assert_eq!(
1093            parse_recipient(b"test.near:unwrap").unwrap(),
1094            Recipient {
1095                receiver_account_id: "test.near".parse().unwrap(),
1096                message: Some(Message::UnwrapWnear),
1097            }
1098        );
1099
1100        assert_eq!(
1101            parse_recipient(
1102                b"e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2:unwrap"
1103            )
1104            .unwrap(),
1105            Recipient {
1106                receiver_account_id:
1107                    "e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2"
1108                        .parse()
1109                        .unwrap(),
1110                message: Some(Message::UnwrapWnear),
1111            }
1112        );
1113
1114        assert_eq!(
1115            parse_recipient(b"test.near:some_msg:with_extra_colon").unwrap(),
1116            Recipient {
1117                receiver_account_id: "test.near".parse().unwrap(),
1118                message: Some(Message::Omni("some_msg:with_extra_colon")),
1119            }
1120        );
1121
1122        assert_eq!(
1123            parse_recipient(b"test.near:").unwrap(),
1124            Recipient {
1125                receiver_account_id: "test.near".parse().unwrap(),
1126                message: Some(Message::Omni("")),
1127            }
1128        );
1129    }
1130
1131    #[test]
1132    fn test_parse_invalid_recipient() {
1133        assert!(parse_recipient(b"test@.near").is_err());
1134        assert!(parse_recipient(b"test@.near:msg").is_err());
1135        assert!(parse_recipient(&[0xc2]).is_err());
1136    }
1137
1138    #[test]
1139    fn test_parse_input() {
1140        #[cfg(feature = "error_refund")]
1141        let refund_address = Address::zero();
1142        let amount = U256::from(100);
1143        let input = [
1144            &[1],
1145            #[cfg(feature = "error_refund")]
1146            refund_address.as_bytes(),
1147            amount.to_big_endian().as_slice(),
1148            b"test.near",
1149        ]
1150        .concat();
1151        assert!(parse_input(&input).is_ok());
1152
1153        let input = [
1154            &[0],
1155            #[cfg(feature = "error_refund")]
1156            refund_address.as_bytes(),
1157            b"test.near:unwrap".as_slice(),
1158        ]
1159        .concat();
1160        assert!(parse_input(&input).is_ok());
1161
1162        let input = [
1163            &[1],
1164            #[cfg(feature = "error_refund")]
1165            refund_address.as_bytes(),
1166            amount.to_big_endian().as_slice(),
1167            b"e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2:unwrap",
1168        ]
1169        .concat();
1170        assert!(parse_input(&input).is_ok());
1171
1172        let input = [
1173            &[1], // flag
1174            #[cfg(feature = "error_refund")]
1175            refund_address.as_bytes(),
1176            amount.to_big_endian().as_slice(), // amount
1177            b"e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2:unwrap",
1178        ]
1179        .concat();
1180        assert!(parse_input(&input).is_ok());
1181    }
1182
1183    #[test]
1184    fn test_parse_exit_to_near_params() {
1185        let amount = U256::from(100);
1186        #[cfg(feature = "error_refund")]
1187        let refund_address = Address::from_array([1; 20]);
1188
1189        let assert_input = |input: Vec<u8>, expected| {
1190            let actual = ExitToNearParams::try_from(input.as_slice()).unwrap();
1191            assert_eq!(actual, expected);
1192        };
1193
1194        let input = [
1195            &[0],
1196            #[cfg(feature = "error_refund")]
1197            refund_address.as_bytes(),
1198            b"test.near".as_slice(),
1199        ]
1200        .concat();
1201        assert_input(
1202            input,
1203            ExitToNearParams::BaseToken(BaseTokenParams {
1204                #[cfg(feature = "error_refund")]
1205                refund_address,
1206                receiver_account_id: "test.near".parse().unwrap(),
1207                message: None,
1208            }),
1209        );
1210
1211        let input = [
1212            &[1],
1213            #[cfg(feature = "error_refund")]
1214            refund_address.as_bytes(),
1215            amount.to_big_endian().as_slice(),
1216            b"test.near:unwrap",
1217        ]
1218        .concat();
1219        assert_input(
1220            input,
1221            ExitToNearParams::Erc20TokenParams(Erc20TokenParams {
1222                #[cfg(feature = "error_refund")]
1223                refund_address,
1224                receiver_account_id: "test.near".parse().unwrap(),
1225                amount,
1226                message: Some(Message::UnwrapWnear),
1227            }),
1228        );
1229
1230        let input = [
1231            &[1],
1232            #[cfg(feature = "error_refund")]
1233            refund_address.as_bytes(),
1234            amount.to_big_endian().as_slice(),
1235            b"e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2:unwrap",
1236        ]
1237        .concat();
1238        assert_input(
1239            input,
1240            ExitToNearParams::Erc20TokenParams(Erc20TokenParams {
1241                #[cfg(feature = "error_refund")]
1242                refund_address,
1243                receiver_account_id:
1244                    "e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2"
1245                        .parse()
1246                        .unwrap(),
1247                amount,
1248                message: Some(Message::UnwrapWnear),
1249            }),
1250        );
1251
1252        let input = [
1253            &[1], // flag
1254            #[cfg(feature = "error_refund")]
1255            refund_address.as_bytes(),
1256            amount.to_big_endian().as_slice(), // amount
1257            b"e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2",
1258            b":",
1259            "{\\\"recipient\\\":\\\"eth:013fe02fb1470d0f4ff072f40960658c4ec8139a\\\",\\\"fee\\\":\\\"0\\\",\\\"native_token_fee\\\":\\\"0\\\"}".as_bytes(),
1260        ]
1261        .concat();
1262        assert_input(
1263            input,
1264            ExitToNearParams::Erc20TokenParams(Erc20TokenParams {
1265                #[cfg(feature = "error_refund")]
1266                refund_address,
1267                receiver_account_id:
1268                    "e523efec9b66c4c8f6e708f1cf56be1399181e5b7c1e35f845670429faf143c2"
1269                        .parse()
1270                        .unwrap(),
1271                amount,
1272                message: Some(Message::Omni(
1273                    "{\\\"recipient\\\":\\\"eth:013fe02fb1470d0f4ff072f40960658c4ec8139a\\\",\\\"fee\\\":\\\"0\\\",\\\"native_token_fee\\\":\\\"0\\\"}",
1274                )),
1275            }),
1276        );
1277    }
1278
1279    #[test]
1280    fn test_ft_transfer_call_args() {
1281        let receiver_id = "test.near".parse().unwrap();
1282        let amount = U256::from(100);
1283        let msg = "some message";
1284
1285        let args = super::ft_transfer_call_args(&receiver_id, amount, msg).unwrap();
1286        let expected =
1287            format!(r#"{{"receiver_id":"{receiver_id}","amount":"{amount}","msg":"{msg}"}}"#,);
1288        assert_eq!(args, expected);
1289    }
1290
1291    #[test]
1292    fn test_ft_transfer_call_args_json_injection() {
1293        let receiver_id = "test.near".parse().unwrap();
1294        let amount = U256::from(100);
1295        let msg = "some message\", \"amount\": \"1000"; // attempt to increase amount
1296
1297        let args = super::ft_transfer_call_args(&receiver_id, amount, msg).unwrap();
1298        let expected = format!(
1299            r#"{{"receiver_id":"{receiver_id}","amount":"{amount}","msg":"some message\", \"amount\": \"1000"}}"#
1300        );
1301        assert_eq!(args, expected);
1302    }
1303
1304    #[test]
1305    #[should_panic(expected = "ERR_INVALID_AMOUNT")]
1306    fn test_ft_transfer_call_args_u256() {
1307        let receiver_id = "test.near".parse().unwrap();
1308        let amount = U256::from(u128::MAX) + 1;
1309        let msg = "some message";
1310        let _ = super::ft_transfer_call_args(&receiver_id, amount, msg).unwrap();
1311    }
1312}