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