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 pub(super) const EXIT_TO_NEAR_GAS: EthGas = EthGas::new(0);
46
47 pub(super) const EXIT_TO_ETHEREUM_GAS: EthGas = EthGas::new(0);
49
50 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 pub(super) const EXIT_TO_NEAR_CALLBACK_GAS: NearGas = NearGas::new(10_000_000_000_000);
58
59 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 pub const EXIT_TO_NEAR_SIGNATURE: H256 = crate::make_h256(
68 0x5a91b8bc9c1981673db8fb226dbd8fcd,
69 0xd0c23f45cd28abb31403a5392f6dd0c7,
70 );
71 pub const EXIT_TO_ETH_SIGNATURE: H256 = crate::make_h256(
73 0xd046c2bb01a5622bc4b9696332391d87,
74 0x491373762eeac0831c48400e2d5a5f07,
75 );
76
77 pub const ETH_ADDRESS: Address = Address::zero();
82
83 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(ðabi::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(ðabi::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 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
263pub 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 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 if let Some(target_gas) = target_gas {
404 if Self::required_gas(input)? > target_gas {
405 return Err(ExitError::OutOfGas);
406 }
407 }
408
409 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 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 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 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 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; 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 Some(Message::UnwrapWnear) if erc20_address == get_wnear_address(io).raw() =>
585 {
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 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 _ => {
625 (
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 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 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 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 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 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 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 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 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 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 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 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], #[cfg(feature = "error_refund")]
1174 refund_address.as_bytes(),
1175 amount.to_big_endian().as_slice(), 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], #[cfg(feature = "error_refund")]
1254 refund_address.as_bytes(),
1255 amount.to_big_endian().as_slice(), 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"; 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}