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 pub(super) const EXIT_TO_NEAR_GAS: EthGas = EthGas::new(0);
43
44 pub(super) const EXIT_TO_ETHEREUM_GAS: EthGas = EthGas::new(0);
46
47 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 pub(super) const EXIT_TO_NEAR_CALLBACK_GAS: NearGas = NearGas::new(10_000_000_000_000);
55
56 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 pub const EXIT_TO_NEAR_SIGNATURE: H256 = crate::make_h256(
65 0x5a91b8bc9c1981673db8fb226dbd8fcd,
66 0xd0c23f45cd28abb31403a5392f6dd0c7,
67 );
68 pub const EXIT_TO_ETH_SIGNATURE: H256 = crate::make_h256(
70 0xd046c2bb01a5622bc4b9696332391d87,
71 0x491373762eeac0831c48400e2d5a5f07,
72 );
73
74 pub const ETH_ADDRESS: Address = Address::zero();
79
80 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(ðabi::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(ðabi::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 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
260pub 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 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 if let Some(target_gas) = target_gas {
407 if Self::required_gas(input)? > target_gas {
408 return Err(ExitError::OutOfGas);
409 }
410 }
411
412 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 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 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 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 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; 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 if erc20_address == get_wnear_address(io).raw() {
593 (
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 Some(Message::Omni(msg)) => (
615 nep141_account_id,
616 format!(
617 r#"{{"receiver_id":"{}","amount":"{}","msg":"{msg}"}}"#,
618 exit_params.receiver_account_id, 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 None => {
633 (
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 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 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 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 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 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 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 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 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 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 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 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], #[cfg(feature = "error_refund")]
1177 refund_address.as_bytes(),
1178 amount.to_big_endian().as_slice(), 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], #[cfg(feature = "error_refund")]
1257 refund_address.as_bytes(),
1258 amount.to_big_endian().as_slice(), 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}