aurora_engine_precompiles/
native.rs

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