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