sui_bridge/
abi.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::encoding::{
5    ADD_TOKENS_ON_EVM_MESSAGE_VERSION, ASSET_PRICE_UPDATE_MESSAGE_VERSION, BridgeMessageEncoding,
6    COMMITTEE_BLOCKLIST_MESSAGE_VERSION, EMERGENCY_BUTTON_MESSAGE_VERSION,
7    EVM_CONTRACT_UPGRADE_MESSAGE_VERSION, LIMIT_UPDATE_MESSAGE_VERSION,
8    TOKEN_TRANSFER_MESSAGE_VERSION_V1, TOKEN_TRANSFER_MESSAGE_VERSION_V2,
9};
10use crate::error::{BridgeError, BridgeResult};
11use crate::types::{
12    AddTokensOnEvmAction, AssetPriceUpdateAction, BlocklistCommitteeAction, BridgeAction,
13    BridgeActionType, EmergencyAction, EthLog, EthToSuiBridgeAction, EthToSuiTokenTransferV2,
14    EvmContractUpgradeAction, LimitUpdateAction, ParsedTokenTransferMessage, SuiToEthBridgeAction,
15    SuiToEthTokenTransfer, SuiToEthTokenTransferV2,
16};
17use alloy::primitives::{Address as EthAddress, TxHash};
18use alloy::rpc::types::eth::Log;
19use alloy::sol;
20use alloy::sol_types::SolEventInterface;
21use serde::{Deserialize, Serialize};
22use sui_types::base_types::SuiAddress;
23use sui_types::bridge::BridgeChainId;
24
25macro_rules! gen_eth_events {
26    // Contracts with Events
27    ($($contract:ident, $module:ident, $contract_event:ident, $abi_path:literal),* $(,)?) => {
28        $(
29            // We must isolate the code sol! generates in a module or we will get an error for
30            // BridgeUtils being duplicated
31            pub mod $module {
32                alloy::sol!(
33                    #[sol(rpc, all_derives, extra_derives(serde::Serialize, serde::Deserialize))]
34                    $contract,
35                    $abi_path,
36                );
37            }
38
39            // Re-export everything to the main scope
40            pub use $module::$contract;
41            #[allow(ambiguous_glob_reexports)]
42            pub use $module::$contract::*;
43        )*
44
45        #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46        pub enum EthBridgeEvent {
47            $(
48                $contract_event($contract_event),
49            )*
50        }
51
52        impl EthBridgeEvent {
53            pub fn try_from_eth_log(log: &EthLog) -> Option<EthBridgeEvent> {
54                Self::try_from_log(&log.log)
55            }
56
57            pub fn try_from_log(log: &Log) -> Option<EthBridgeEvent> {
58                $(
59                    if let Ok(decoded) = $contract_event::decode_raw_log(log.topics(), &log.data().data) {
60                        return Some(EthBridgeEvent::$contract_event(decoded));
61                    }
62                )*
63
64                None
65            }
66        }
67    };
68
69    // Contracts without Events
70    ($($contract:ident, $module:ident, $abi_path:literal),* $(,)?) => {
71        $(
72            pub mod $module {
73                alloy::sol!(
74                    #[sol(rpc, extra_derives(serde::Serialize, serde::Deserialize))]
75                    $contract,
76                    $abi_path,
77                );
78            }
79
80            pub use $module::$contract;
81        )*
82    };
83}
84
85#[rustfmt::skip]
86gen_eth_events!(
87    // Format: ContractStruct, ModuleName, EventStruct, AbiPath
88    EthSuiBridge, eth_sui_bridge, EthSuiBridgeEvents, "abi/sui_bridge.json",
89    EthBridgeCommittee, eth_bridge_committee, EthBridgeCommitteeEvents, "abi/bridge_committee.json",
90    EthBridgeLimiter, eth_bridge_limiter, EthBridgeLimiterEvents, "abi/bridge_limiter.json",
91    EthBridgeConfig, eth_bridge_config, EthBridgeConfigEvents, "abi/bridge_config.json",
92    EthCommitteeUpgradeableContract, eth_committee_upgradeable_contract, EthCommitteeUpgradeableContractEvents, "abi/bridge_committee_upgradeable.json",
93);
94
95gen_eth_events!(
96    // Format: ContractStruct, ModuleName, AbiPath
97    EthBridgeVault,
98    eth_bridge_vault,
99    "abi/bridge_vault.json"
100);
101
102sol!(
103    #[sol(rpc, extra_derives(serde::Serialize, serde::Deserialize))]
104    EthERC20,
105    "abi/erc20.json",
106);
107
108impl EthBridgeEvent {
109    pub fn try_into_bridge_action(
110        self,
111        eth_tx_hash: TxHash,
112        eth_event_index: u16,
113    ) -> BridgeResult<Option<BridgeAction>> {
114        Ok(match self {
115            EthBridgeEvent::EthSuiBridgeEvents(event) => {
116                match event {
117                    EthSuiBridgeEvents::TokensDeposited(event) => {
118                        let bridge_event = match EthToSuiTokenBridgeV1::try_from(&event) {
119                            Ok(bridge_event) => {
120                                if bridge_event.sui_adjusted_amount == 0 {
121                                    return Err(BridgeError::ZeroValueBridgeTransfer(format!(
122                                        "Manual intervention is required: {}",
123                                        eth_tx_hash
124                                    )));
125                                }
126                                bridge_event
127                            }
128                            // This only happens when solidity code does not align with rust code.
129                            // When this happens in production, there is a risk of stuck bridge transfers.
130                            // We log error here.
131                            // TODO: add metrics and alert
132                            Err(e) => {
133                                return Err(BridgeError::Generic(format!(
134                                    "Manual intervention is required. Failed to convert TokensDepositedFilter log to EthToSuiTokenBridgeV1. This indicates incorrect parameters or a bug in the code: {:?}. Err: {:?}",
135                                    event, e
136                                )));
137                            }
138                        };
139
140                        Some(BridgeAction::EthToSuiBridgeAction(EthToSuiBridgeAction {
141                            eth_tx_hash,
142                            eth_event_index,
143                            eth_bridge_event: bridge_event,
144                        }))
145                    }
146                    EthSuiBridgeEvents::TokensDepositedV2(event) => {
147                        let eth_bridge_event = EthToSuiTokenBridgeV2::try_from(&event)?;
148                        Some(BridgeAction::EthToSuiTokenTransferV2(
149                            EthToSuiTokenTransferV2 {
150                                eth_tx_hash,
151                                eth_event_index,
152                                eth_bridge_event,
153                            },
154                        ))
155                    }
156                    EthSuiBridgeEvents::TokensClaimed(_event) => None,
157                    EthSuiBridgeEvents::Paused(_event) => None,
158                    EthSuiBridgeEvents::Unpaused(_event) => None,
159                    EthSuiBridgeEvents::Upgraded(_event) => None,
160                    EthSuiBridgeEvents::Initialized(_event) => None,
161                    EthSuiBridgeEvents::ContractUpgraded(_event) => None,
162                    EthSuiBridgeEvents::EmergencyOperation(_event) => None,
163                }
164            }
165            EthBridgeEvent::EthBridgeCommitteeEvents(event) => match event {
166                EthBridgeCommitteeEvents::BlocklistUpdated(_event) => None,
167                EthBridgeCommitteeEvents::Initialized(_event) => None,
168                EthBridgeCommitteeEvents::Upgraded(_event) => None,
169                EthBridgeCommitteeEvents::BlocklistUpdatedV2(_event) => None,
170                EthBridgeCommitteeEvents::ContractUpgraded(_event) => None,
171            },
172            EthBridgeEvent::EthBridgeLimiterEvents(event) => match event {
173                EthBridgeLimiterEvents::LimitUpdated(_event) => None,
174                EthBridgeLimiterEvents::Initialized(_event) => None,
175                EthBridgeLimiterEvents::Upgraded(_event) => None,
176                EthBridgeLimiterEvents::HourlyTransferAmountUpdated(_event) => None,
177                EthBridgeLimiterEvents::OwnershipTransferred(_event) => None,
178                EthBridgeLimiterEvents::ContractUpgraded(_event) => None,
179                EthBridgeLimiterEvents::LimitUpdatedV2(_event) => None,
180            },
181            EthBridgeEvent::EthBridgeConfigEvents(event) => match event {
182                EthBridgeConfigEvents::Initialized(_event) => None,
183                EthBridgeConfigEvents::Upgraded(_event) => None,
184                EthBridgeConfigEvents::TokenAdded(_event) => None,
185                EthBridgeConfigEvents::TokenPriceUpdated(_event) => None,
186                EthBridgeConfigEvents::ContractUpgraded(_event) => None,
187                EthBridgeConfigEvents::TokenPriceUpdatedV2(_event) => None,
188                EthBridgeConfigEvents::TokensAddedV2(_event) => None,
189            },
190            EthBridgeEvent::EthCommitteeUpgradeableContractEvents(event) => match event {
191                EthCommitteeUpgradeableContractEvents::Initialized(_event) => None,
192                EthCommitteeUpgradeableContractEvents::Upgraded(_event) => None,
193            },
194        })
195    }
196}
197
198/// The event emitted when tokens are deposited into the bridge on Ethereum.
199/// Sanity checked version of TokensDepositedFilter
200#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
201pub struct EthToSuiTokenBridgeV1 {
202    pub nonce: u64,
203    pub sui_chain_id: BridgeChainId,
204    pub eth_chain_id: BridgeChainId,
205    pub sui_address: SuiAddress,
206    pub eth_address: EthAddress,
207    pub token_id: u8,
208    pub sui_adjusted_amount: u64,
209}
210
211impl TryFrom<&TokensDeposited> for EthToSuiTokenBridgeV1 {
212    type Error = BridgeError;
213
214    fn try_from(event: &TokensDeposited) -> BridgeResult<Self> {
215        Ok(Self {
216            nonce: event.nonce,
217            sui_chain_id: BridgeChainId::try_from(event.destinationChainID)?,
218            eth_chain_id: BridgeChainId::try_from(event.sourceChainID)?,
219            sui_address: SuiAddress::from_bytes(event.recipientAddress.as_ref())?,
220            eth_address: event.senderAddress,
221            token_id: event.tokenID,
222            sui_adjusted_amount: event.suiAdjustedAmount,
223        })
224    }
225}
226
227/// Sanity checked version of TokensDepositedV2Filter that captures the new timestamp.
228#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
229pub struct EthToSuiTokenBridgeV2 {
230    pub nonce: u64,
231    pub sui_chain_id: BridgeChainId,
232    pub eth_chain_id: BridgeChainId,
233    pub sui_address: SuiAddress,
234    pub eth_address: EthAddress,
235    pub token_id: u8,
236    pub sui_adjusted_amount: u64,
237    pub timestamp_seconds: u64,
238}
239
240impl TryFrom<&TokensDepositedV2> for EthToSuiTokenBridgeV2 {
241    type Error = BridgeError;
242
243    fn try_from(event: &TokensDepositedV2) -> BridgeResult<Self> {
244        Ok(Self {
245            nonce: event.nonce,
246            sui_chain_id: BridgeChainId::try_from(event.destinationChainID)?,
247            eth_chain_id: BridgeChainId::try_from(event.sourceChainID)?,
248            sui_address: SuiAddress::from_bytes(event.recipientAddress.as_ref())?,
249            eth_address: event.senderAddress,
250            token_id: event.tokenID,
251            sui_adjusted_amount: event.suiAdjustedAmount,
252            timestamp_seconds: event.timestampSeconds.to::<u64>(),
253        })
254    }
255}
256
257impl From<EthToSuiTokenBridgeV2> for EthToSuiTokenBridgeV1 {
258    fn from(value: EthToSuiTokenBridgeV2) -> Self {
259        Self {
260            nonce: value.nonce,
261            sui_chain_id: value.sui_chain_id,
262            eth_chain_id: value.eth_chain_id,
263            sui_address: value.sui_address,
264            eth_address: value.eth_address,
265            token_id: value.token_id,
266            sui_adjusted_amount: value.sui_adjusted_amount,
267        }
268    }
269}
270
271////////////////////////////////////////////////////////////////////////
272//                        Eth Message Conversion                      //
273////////////////////////////////////////////////////////////////////////
274
275impl TryFrom<SuiToEthBridgeAction> for eth_sui_bridge::BridgeUtils::Message {
276    type Error = BridgeError;
277
278    fn try_from(action: SuiToEthBridgeAction) -> BridgeResult<Self> {
279        Ok(eth_sui_bridge::BridgeUtils::Message {
280            messageType: BridgeActionType::TokenTransfer as u8,
281            version: TOKEN_TRANSFER_MESSAGE_VERSION_V1,
282            nonce: action.sui_bridge_event.nonce,
283            chainID: action.sui_bridge_event.sui_chain_id as u8,
284            payload: action
285                .as_payload_bytes()
286                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
287                .into(),
288        })
289    }
290}
291
292impl TryFrom<SuiToEthTokenTransfer> for eth_sui_bridge::BridgeUtils::Message {
293    type Error = BridgeError;
294
295    fn try_from(action: SuiToEthTokenTransfer) -> BridgeResult<Self> {
296        Ok(eth_sui_bridge::BridgeUtils::Message {
297            messageType: BridgeActionType::TokenTransfer as u8,
298            version: TOKEN_TRANSFER_MESSAGE_VERSION_V1,
299            nonce: action.nonce,
300            chainID: action.sui_chain_id as u8,
301            payload: action
302                .as_payload_bytes()
303                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
304                .into(),
305        })
306    }
307}
308
309impl TryFrom<SuiToEthTokenTransferV2> for eth_sui_bridge::BridgeUtils::Message {
310    type Error = BridgeError;
311
312    fn try_from(action: SuiToEthTokenTransferV2) -> BridgeResult<Self> {
313        Ok(eth_sui_bridge::BridgeUtils::Message {
314            messageType: BridgeActionType::TokenTransfer as u8,
315            version: TOKEN_TRANSFER_MESSAGE_VERSION_V2,
316            nonce: action.nonce,
317            chainID: action.sui_chain_id as u8,
318            payload: action
319                .as_payload_bytes()
320                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
321                .into(),
322        })
323    }
324}
325
326impl From<ParsedTokenTransferMessage> for eth_sui_bridge::BridgeUtils::Message {
327    fn from(parsed_message: ParsedTokenTransferMessage) -> Self {
328        eth_sui_bridge::BridgeUtils::Message {
329            messageType: BridgeActionType::TokenTransfer as u8,
330            version: parsed_message.message_version,
331            nonce: parsed_message.seq_num,
332            chainID: parsed_message.source_chain as u8,
333            payload: parsed_message.payload.into(),
334        }
335    }
336}
337
338impl TryFrom<EmergencyAction> for eth_sui_bridge::BridgeUtils::Message {
339    type Error = BridgeError;
340
341    fn try_from(action: EmergencyAction) -> BridgeResult<Self> {
342        Ok(eth_sui_bridge::BridgeUtils::Message {
343            messageType: BridgeActionType::EmergencyButton as u8,
344            version: EMERGENCY_BUTTON_MESSAGE_VERSION,
345            nonce: action.nonce,
346            chainID: action.chain_id as u8,
347            payload: action
348                .as_payload_bytes()
349                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
350                .into(),
351        })
352    }
353}
354
355impl TryFrom<BlocklistCommitteeAction> for eth_bridge_committee::BridgeUtils::Message {
356    type Error = BridgeError;
357
358    fn try_from(action: BlocklistCommitteeAction) -> BridgeResult<Self> {
359        Ok(eth_bridge_committee::BridgeUtils::Message {
360            messageType: BridgeActionType::UpdateCommitteeBlocklist as u8,
361            version: COMMITTEE_BLOCKLIST_MESSAGE_VERSION,
362            nonce: action.nonce,
363            chainID: action.chain_id as u8,
364            payload: action
365                .as_payload_bytes()
366                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
367                .into(),
368        })
369    }
370}
371
372impl TryFrom<LimitUpdateAction> for eth_bridge_limiter::BridgeUtils::Message {
373    type Error = BridgeError;
374
375    fn try_from(action: LimitUpdateAction) -> BridgeResult<Self> {
376        Ok(eth_bridge_limiter::BridgeUtils::Message {
377            messageType: BridgeActionType::LimitUpdate as u8,
378            version: LIMIT_UPDATE_MESSAGE_VERSION,
379            nonce: action.nonce,
380            chainID: action.chain_id as u8,
381            payload: action
382                .as_payload_bytes()
383                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
384                .into(),
385        })
386    }
387}
388
389impl TryFrom<AssetPriceUpdateAction> for eth_bridge_config::BridgeUtils::Message {
390    type Error = BridgeError;
391
392    fn try_from(action: AssetPriceUpdateAction) -> BridgeResult<Self> {
393        Ok(eth_bridge_config::BridgeUtils::Message {
394            messageType: BridgeActionType::AssetPriceUpdate as u8,
395            version: ASSET_PRICE_UPDATE_MESSAGE_VERSION,
396            nonce: action.nonce,
397            chainID: action.chain_id as u8,
398            payload: action
399                .as_payload_bytes()
400                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
401                .into(),
402        })
403    }
404}
405
406impl TryFrom<AddTokensOnEvmAction> for eth_bridge_config::BridgeUtils::Message {
407    type Error = BridgeError;
408
409    fn try_from(action: AddTokensOnEvmAction) -> BridgeResult<Self> {
410        Ok(eth_bridge_config::BridgeUtils::Message {
411            messageType: BridgeActionType::AddTokensOnEvm as u8,
412            version: ADD_TOKENS_ON_EVM_MESSAGE_VERSION,
413            nonce: action.nonce,
414            chainID: action.chain_id as u8,
415            payload: action
416                .as_payload_bytes()
417                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
418                .into(),
419        })
420    }
421}
422
423impl TryFrom<EvmContractUpgradeAction>
424    for eth_committee_upgradeable_contract::BridgeUtils::Message
425{
426    type Error = BridgeError;
427
428    fn try_from(action: EvmContractUpgradeAction) -> BridgeResult<Self> {
429        Ok(eth_committee_upgradeable_contract::BridgeUtils::Message {
430            messageType: BridgeActionType::EvmContractUpgrade as u8,
431            version: EVM_CONTRACT_UPGRADE_MESSAGE_VERSION,
432            nonce: action.nonce,
433            chainID: action.chain_id as u8,
434            payload: action
435                .as_payload_bytes()
436                .map_err(|e| BridgeError::Generic(format!("Failed to encode payload: {}", e)))?
437                .into(),
438        })
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::{
446        crypto::BridgeAuthorityPublicKeyBytes,
447        types::{BlocklistType, EmergencyActionType},
448    };
449    use alloy::primitives::{B256, Bytes, LogData};
450    use fastcrypto::encoding::{Encoding, Hex};
451    use hex_literal::hex;
452    use std::str::FromStr;
453    use sui_types::{bridge::TOKEN_ID_ETH, crypto::ToFromBytes};
454
455    #[test]
456    fn test_eth_message_conversion_emergency_action_regression() -> anyhow::Result<()> {
457        telemetry_subscribers::init_for_testing();
458
459        let action = EmergencyAction {
460            nonce: 2,
461            chain_id: BridgeChainId::EthSepolia,
462            action_type: EmergencyActionType::Pause,
463        };
464        let message: eth_sui_bridge::BridgeUtils::Message = action.try_into().unwrap();
465        assert_eq!(
466            message,
467            eth_sui_bridge::BridgeUtils::Message {
468                messageType: BridgeActionType::EmergencyButton as u8,
469                version: EMERGENCY_BUTTON_MESSAGE_VERSION,
470                nonce: 2,
471                chainID: BridgeChainId::EthSepolia as u8,
472                payload: vec![0].into(),
473            }
474        );
475        Ok(())
476    }
477
478    #[test]
479    fn test_eth_message_conversion_update_blocklist_action_regression() -> anyhow::Result<()> {
480        telemetry_subscribers::init_for_testing();
481        let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
482            &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
483                .unwrap(),
484        )
485        .unwrap();
486        let action = BlocklistCommitteeAction {
487            nonce: 0,
488            chain_id: BridgeChainId::EthSepolia,
489            blocklist_type: BlocklistType::Blocklist,
490            members_to_update: vec![pub_key_bytes],
491        };
492        let message: eth_bridge_committee::BridgeUtils::Message = action.try_into().unwrap();
493        assert_eq!(
494            message,
495            eth_bridge_committee::BridgeUtils::Message {
496                messageType: BridgeActionType::UpdateCommitteeBlocklist as u8,
497                version: COMMITTEE_BLOCKLIST_MESSAGE_VERSION,
498                nonce: 0,
499                chainID: BridgeChainId::EthSepolia as u8,
500                payload: Hex::decode("000168b43fd906c0b8f024a18c56e06744f7c6157c65")
501                    .unwrap()
502                    .into(),
503            }
504        );
505        Ok(())
506    }
507
508    #[test]
509    fn test_eth_message_conversion_update_limit_action_regression() -> anyhow::Result<()> {
510        telemetry_subscribers::init_for_testing();
511        let action = LimitUpdateAction {
512            nonce: 2,
513            chain_id: BridgeChainId::EthSepolia,
514            sending_chain_id: BridgeChainId::SuiTestnet,
515            new_usd_limit: 4200000,
516        };
517        let message: eth_bridge_limiter::BridgeUtils::Message = action.try_into().unwrap();
518        assert_eq!(
519            message,
520            eth_bridge_limiter::BridgeUtils::Message {
521                messageType: BridgeActionType::LimitUpdate as u8,
522                version: LIMIT_UPDATE_MESSAGE_VERSION,
523                nonce: 2,
524                chainID: BridgeChainId::EthSepolia as u8,
525                payload: Hex::decode("010000000000401640").unwrap().into(),
526            }
527        );
528        Ok(())
529    }
530
531    #[test]
532    fn test_eth_message_conversion_contract_upgrade_action_regression() -> anyhow::Result<()> {
533        telemetry_subscribers::init_for_testing();
534        let action = EvmContractUpgradeAction {
535            nonce: 2,
536            chain_id: BridgeChainId::EthSepolia,
537            proxy_address: EthAddress::repeat_byte(1),
538            new_impl_address: EthAddress::repeat_byte(2),
539            call_data: Vec::from("deadbeef"),
540        };
541        let message: eth_committee_upgradeable_contract::BridgeUtils::Message =
542            action.try_into().unwrap();
543        assert_eq!(
544            message,
545            eth_committee_upgradeable_contract::BridgeUtils::Message {
546                messageType: BridgeActionType::EvmContractUpgrade as u8,
547                version: EVM_CONTRACT_UPGRADE_MESSAGE_VERSION,
548                nonce: 2,
549                chainID: BridgeChainId::EthSepolia as u8,
550                payload: Hex::decode("0x00000000000000000000000001010101010101010101010101010101010101010000000000000000000000000202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000086465616462656566000000000000000000000000000000000000000000000000").unwrap().into(),
551            }
552        );
553        Ok(())
554    }
555
556    #[test]
557    fn test_eth_message_conversion_update_price_action_regression() -> anyhow::Result<()> {
558        telemetry_subscribers::init_for_testing();
559        let action = AssetPriceUpdateAction {
560            nonce: 2,
561            chain_id: BridgeChainId::EthSepolia,
562            token_id: TOKEN_ID_ETH,
563            new_usd_price: 80000000,
564        };
565        let message: eth_bridge_config::BridgeUtils::Message = action.try_into().unwrap();
566        assert_eq!(
567            message,
568            eth_bridge_config::BridgeUtils::Message {
569                messageType: BridgeActionType::AssetPriceUpdate as u8,
570                version: ASSET_PRICE_UPDATE_MESSAGE_VERSION,
571                nonce: 2,
572                chainID: BridgeChainId::EthSepolia as u8,
573                payload: Hex::decode("020000000004c4b400").unwrap().into(),
574            }
575        );
576        Ok(())
577    }
578
579    #[test]
580    fn test_eth_message_conversion_add_tokens_on_evm_action_regression() -> anyhow::Result<()> {
581        let action = AddTokensOnEvmAction {
582            nonce: 5,
583            chain_id: BridgeChainId::EthCustom,
584            native: true,
585            token_ids: vec![99, 100, 101],
586            token_addresses: vec![
587                EthAddress::repeat_byte(1),
588                EthAddress::repeat_byte(2),
589                EthAddress::repeat_byte(3),
590            ],
591            token_sui_decimals: vec![5, 6, 7],
592            token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
593        };
594        let message: eth_bridge_config::BridgeUtils::Message = action.try_into().unwrap();
595        assert_eq!(
596            message,
597            eth_bridge_config::BridgeUtils::Message {
598                messageType: BridgeActionType::AddTokensOnEvm as u8,
599                version: ADD_TOKENS_ON_EVM_MESSAGE_VERSION,
600                nonce: 5,
601                chainID: BridgeChainId::EthCustom as u8,
602                payload: Hex::decode("0103636465030101010101010101010101010101010101010101020202020202020202020202020202020202020203030303030303030303030303030303030303030305060703000000003b9aca00000000007735940000000000b2d05e00").unwrap().into(),
603            }
604        );
605        Ok(())
606    }
607
608    #[test]
609    fn test_token_deposit_eth_log_to_sui_bridge_event_regression() -> anyhow::Result<()> {
610        telemetry_subscribers::init_for_testing();
611        let tx_hash = TxHash::random();
612        let topics: Vec<B256> = vec![
613            hex!("a0f1d54820817ede8517e70a3d0a9197c015471c5360d2119b759f0359858ce6").into(),
614            hex!("000000000000000000000000000000000000000000000000000000000000000c").into(),
615            hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
616            hex!("0000000000000000000000000000000000000000000000000000000000000002").into(),
617        ];
618        let encoded =
619            Hex::decode("0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000fa56ea0000000000000000000000000014dc79964da2c08b23698b3d3cc7ca32193d9955000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000203b1eb23133e94d08d0da9303cfd38e7d4f8f6951f235daa62cd64ea5b6d96d77").unwrap();
620
621        let log_data = LogData::new(topics, encoded.into()).unwrap();
622        let action = EthLog {
623            block_number: 33,
624            tx_hash,
625            log_index_in_tx: 1,
626            log: Log {
627                inner: alloy::primitives::Log {
628                    address: EthAddress::repeat_byte(1),
629                    data: log_data,
630                },
631                block_hash: None,
632                block_number: None,
633                transaction_hash: Some(tx_hash),
634                transaction_index: Some(0),
635                log_index: Some(1),
636                ..Default::default()
637            },
638        };
639        let event = EthBridgeEvent::try_from_eth_log(&action).unwrap();
640        assert_eq!(
641            event,
642            EthBridgeEvent::EthSuiBridgeEvents(EthSuiBridgeEvents::TokensDeposited(
643                TokensDeposited {
644                    sourceChainID: 12,
645                    nonce: 0,
646                    destinationChainID: 2,
647                    tokenID: 2,
648                    suiAdjustedAmount: 4200000000,
649                    senderAddress: EthAddress::from_str(
650                        "0x14dc79964da2c08b23698b3d3cc7ca32193d9955"
651                    )
652                    .unwrap(),
653                    recipientAddress: Bytes::from(
654                        Hex::decode(
655                            "0x3b1eb23133e94d08d0da9303cfd38e7d4f8f6951f235daa62cd64ea5b6d96d77"
656                        )
657                        .unwrap(),
658                    ),
659                }
660            ))
661        );
662        Ok(())
663    }
664
665    #[test]
666    fn test_0_sui_amount_conversion_for_eth_event() {
667        let e = EthBridgeEvent::EthSuiBridgeEvents(EthSuiBridgeEvents::TokensDeposited(
668            TokensDeposited {
669                sourceChainID: BridgeChainId::EthSepolia as u8,
670                nonce: 0,
671                destinationChainID: BridgeChainId::SuiTestnet as u8,
672                tokenID: 2,
673                suiAdjustedAmount: 1,
674                senderAddress: EthAddress::random(),
675                recipientAddress: Bytes::from(SuiAddress::random_for_testing_only().to_vec()),
676            },
677        ));
678        assert!(
679            e.try_into_bridge_action(TxHash::random(), 0)
680                .unwrap()
681                .is_some()
682        );
683
684        let e = EthBridgeEvent::EthSuiBridgeEvents(EthSuiBridgeEvents::TokensDeposited(
685            TokensDeposited {
686                sourceChainID: BridgeChainId::EthSepolia as u8,
687                nonce: 0,
688                destinationChainID: BridgeChainId::SuiTestnet as u8,
689                tokenID: 2,
690                suiAdjustedAmount: 0, // <------------
691                senderAddress: EthAddress::random(),
692                recipientAddress: Bytes::from(SuiAddress::random_for_testing_only().to_vec()),
693            },
694        ));
695        match e.try_into_bridge_action(TxHash::random(), 0).unwrap_err() {
696            BridgeError::ZeroValueBridgeTransfer(_) => {}
697            e => panic!("Unexpected error: {:?}", e),
698        }
699    }
700}