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