sui_bridge/
types.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::abi::{EthToSuiTokenBridgeV1, EthToSuiTokenBridgeV2};
5use crate::crypto::BridgeAuthorityPublicKeyBytes;
6use crate::crypto::{
7    BridgeAuthorityPublicKey, BridgeAuthorityRecoverableSignature, BridgeAuthoritySignInfo,
8};
9use crate::encoding::BridgeMessageEncoding;
10use crate::error::{BridgeError, BridgeResult};
11use crate::events::EmittedSuiToEthTokenBridgeV1;
12use alloy::primitives::{Address as EthAddress, B256, TxHash as EthTransactionHash};
13use alloy::rpc::types::eth::Log;
14use enum_dispatch::enum_dispatch;
15use fastcrypto::encoding::{Encoding, Hex};
16use fastcrypto::hash::{HashFunction, Keccak256};
17use num_enum::TryFromPrimitive;
18use rand::Rng;
19use rand::seq::SliceRandom;
20use serde::{Deserialize, Serialize};
21use shared_crypto::intent::IntentScope;
22use std::collections::{BTreeMap, BTreeSet};
23use std::fmt::Debug;
24use strum_macros::Display;
25use sui_types::TypeTag;
26use sui_types::base_types::SuiAddress;
27use sui_types::bridge::{
28    APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM, APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI,
29    BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER, BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER, BridgeChainId,
30    MoveTypeBridgeMessage, MoveTypeBridgeRecord, MoveTypeTokenTransferPayload,
31};
32use sui_types::bridge::{
33    APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE, APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST,
34    APPROVAL_THRESHOLD_EMERGENCY_PAUSE, APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE,
35    APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE, APPROVAL_THRESHOLD_LIMIT_UPDATE,
36    APPROVAL_THRESHOLD_TOKEN_TRANSFER, MoveTypeParsedTokenTransferMessage,
37};
38use sui_types::committee::CommitteeTrait;
39use sui_types::committee::StakeUnit;
40use sui_types::crypto::ToFromBytes;
41use sui_types::digests::{Digest, TransactionDigest};
42use sui_types::message_envelope::{Envelope, Message, VerifiedEnvelope};
43
44pub const BRIDGE_AUTHORITY_TOTAL_VOTING_POWER: u64 = 10000;
45
46pub const USD_MULTIPLIER: u64 = 10000; // decimal places = 4
47
48pub type IsBridgePaused = bool;
49pub const BRIDGE_PAUSED: bool = true;
50pub const BRIDGE_UNPAUSED: bool = false;
51
52#[derive(Debug, Eq, PartialEq, Clone)]
53pub struct BridgeAuthority {
54    pub sui_address: SuiAddress,
55    pub pubkey: BridgeAuthorityPublicKey,
56    pub voting_power: u64,
57    pub base_url: String,
58    pub is_blocklisted: bool,
59}
60
61impl BridgeAuthority {
62    pub fn pubkey_bytes(&self) -> BridgeAuthorityPublicKeyBytes {
63        BridgeAuthorityPublicKeyBytes::from(&self.pubkey)
64    }
65}
66
67#[derive(Debug, Clone)]
68pub struct BridgeCommittee {
69    members: BTreeMap<BridgeAuthorityPublicKeyBytes, BridgeAuthority>,
70    total_blocklisted_stake: StakeUnit,
71}
72
73impl BridgeCommittee {
74    pub fn new(members: Vec<BridgeAuthority>) -> BridgeResult<Self> {
75        let mut members_map = BTreeMap::new();
76        let mut total_blocklisted_stake = 0;
77        let mut total_stake = 0;
78        for member in members {
79            let public_key = BridgeAuthorityPublicKeyBytes::from(&member.pubkey);
80            if members_map.contains_key(&public_key) {
81                return Err(BridgeError::InvalidBridgeCommittee(
82                    "Duplicate BridgeAuthority Public key".into(),
83                ));
84            }
85            // TODO: should we disallow identical network addresses?
86            if member.is_blocklisted {
87                total_blocklisted_stake += member.voting_power;
88            }
89            total_stake += member.voting_power;
90            members_map.insert(public_key, member);
91        }
92        if total_stake < BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER {
93            return Err(BridgeError::InvalidBridgeCommittee(format!(
94                "Total voting power is below minimal {BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER}"
95            )));
96        }
97        if total_stake > BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER {
98            return Err(BridgeError::InvalidBridgeCommittee(format!(
99                "Total voting power is above maximal {BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER}"
100            )));
101        }
102        Ok(Self {
103            members: members_map,
104            total_blocklisted_stake,
105        })
106    }
107
108    pub fn is_active_member(&self, member: &BridgeAuthorityPublicKeyBytes) -> bool {
109        self.members.contains_key(member) && !self.members.get(member).unwrap().is_blocklisted
110    }
111
112    pub fn members(&self) -> &BTreeMap<BridgeAuthorityPublicKeyBytes, BridgeAuthority> {
113        &self.members
114    }
115
116    pub fn member(&self, member: &BridgeAuthorityPublicKeyBytes) -> Option<&BridgeAuthority> {
117        self.members.get(member)
118    }
119
120    pub fn total_blocklisted_stake(&self) -> StakeUnit {
121        self.total_blocklisted_stake
122    }
123
124    pub fn active_stake(&self, member: &BridgeAuthorityPublicKeyBytes) -> StakeUnit {
125        self.members
126            .get(member)
127            .map(|a| if a.is_blocklisted { 0 } else { a.voting_power })
128            .unwrap_or(0)
129    }
130}
131
132impl core::fmt::Display for BridgeCommittee {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
134        for m in self.members.values() {
135            writeln!(
136                f,
137                "pubkey: {:?}, url: {:?}, stake: {:?}, blocklisted: {}, eth address: {:x}",
138                Hex::encode(m.pubkey_bytes().as_bytes()),
139                m.base_url,
140                m.voting_power,
141                m.is_blocklisted,
142                m.pubkey_bytes().to_eth_address(),
143            )?;
144        }
145        Ok(())
146    }
147}
148
149impl CommitteeTrait<BridgeAuthorityPublicKeyBytes> for BridgeCommittee {
150    // Note: blocklisted members are always excluded.
151    fn shuffle_by_stake_with_rng(
152        &self,
153        // `preferences` is used as a *flag* here to influence the order of validators to be requested.
154        //  * if `Some(_)`, then we will request validators in the order of the voting power
155        //  * if `None`, we still refer to voting power, but they are shuffled by randomness.
156        //  to save gas cost.
157        preferences: Option<&BTreeSet<BridgeAuthorityPublicKeyBytes>>,
158        // only attempt from these authorities.
159        restrict_to: Option<&BTreeSet<BridgeAuthorityPublicKeyBytes>>,
160        rng: &mut impl Rng,
161    ) -> Vec<BridgeAuthorityPublicKeyBytes> {
162        let mut candidates = self
163            .members
164            .iter()
165            .filter_map(|(name, a)| {
166                // Remove blocklisted members
167                if a.is_blocklisted {
168                    return None;
169                }
170                // exclude non-allowlisted members
171                if let Some(restrict_to) = restrict_to {
172                    match restrict_to.contains(name) {
173                        true => Some((name.clone(), a.voting_power)),
174                        false => None,
175                    }
176                } else {
177                    Some((name.clone(), a.voting_power))
178                }
179            })
180            .collect::<Vec<_>>();
181        if preferences.is_some() {
182            candidates.sort_by(|(_, a), (_, b)| b.cmp(a));
183            candidates.iter().map(|(name, _)| name.clone()).collect()
184        } else {
185            candidates
186                .choose_multiple_weighted(rng, candidates.len(), |(_, weight)| *weight as f64)
187                // Unwrap safe: it panics when the third parameter is larger than the size of the slice
188                .unwrap()
189                .map(|(name, _)| name)
190                .cloned()
191                .collect()
192        }
193    }
194
195    fn weight(&self, author: &BridgeAuthorityPublicKeyBytes) -> StakeUnit {
196        self.members
197            .get(author)
198            .map(|a| a.voting_power)
199            .unwrap_or(0)
200    }
201}
202
203#[derive(Serialize, Copy, Clone, PartialEq, Eq, TryFromPrimitive, Hash, Display)]
204#[repr(u8)]
205pub enum BridgeActionType {
206    TokenTransfer = 0,
207    UpdateCommitteeBlocklist = 1,
208    EmergencyButton = 2,
209    LimitUpdate = 3,
210    AssetPriceUpdate = 4,
211    EvmContractUpgrade = 5,
212    AddTokensOnSui = 6,
213    AddTokensOnEvm = 7,
214}
215
216#[derive(Clone, PartialEq, Eq)]
217pub struct BridgeActionKey {
218    pub chain_id: BridgeChainId,
219    pub action_type: BridgeActionType,
220    pub seq_num: u64,
221}
222
223impl Debug for BridgeActionKey {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        write!(
226            f,
227            "BridgeActionKey({},{},{})",
228            self.action_type as u8, self.chain_id as u8, self.seq_num
229        )
230    }
231}
232
233#[derive(Debug, PartialEq, Eq, Clone, TryFromPrimitive)]
234#[repr(u8)]
235pub enum BridgeActionStatus {
236    Pending = 0,
237    Approved = 1,
238    Claimed = 2,
239    NotFound = 3,
240}
241
242#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
243pub struct SuiToEthBridgeAction {
244    // Digest of the transaction where the event was emitted
245    pub sui_tx_digest: TransactionDigest,
246    // The index of the event in the transaction
247    pub sui_tx_event_index: u16,
248    pub sui_bridge_event: EmittedSuiToEthTokenBridgeV1,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
252pub struct SuiToEthTokenTransfer {
253    pub nonce: u64,
254    pub sui_chain_id: BridgeChainId,
255    pub eth_chain_id: BridgeChainId,
256    pub sui_address: SuiAddress,
257    pub eth_address: EthAddress,
258    pub token_id: u8,
259    // The amount of tokens deposited with decimal points on Sui side
260    pub amount_adjusted: u64,
261}
262
263#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
264pub struct SuiToEthTokenTransferV2 {
265    pub nonce: u64,
266    pub sui_chain_id: BridgeChainId,
267    pub eth_chain_id: BridgeChainId,
268    pub sui_address: SuiAddress,
269    pub eth_address: EthAddress,
270    pub token_id: u8,
271    // The amount of tokens deposited with decimal points on Sui side
272    pub amount_adjusted: u64,
273    pub timestamp_ms: u64,
274}
275
276#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
277pub struct EthToSuiBridgeAction {
278    // Digest of the transaction where the event was emitted
279    pub eth_tx_hash: EthTransactionHash,
280    // The index of the event in the transaction
281    pub eth_event_index: u16,
282    pub eth_bridge_event: EthToSuiTokenBridgeV1,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
286pub struct EthToSuiTokenTransferV2 {
287    // Digest of the transaction where the event was emitted
288    pub eth_tx_hash: EthTransactionHash,
289    // The index of the event in the transaction
290    pub eth_event_index: u16,
291    pub eth_bridge_event: EthToSuiTokenBridgeV2,
292}
293
294#[derive(
295    Debug,
296    Serialize,
297    Deserialize,
298    PartialEq,
299    Eq,
300    Clone,
301    Copy,
302    TryFromPrimitive,
303    Hash,
304    clap::ValueEnum,
305)]
306#[repr(u8)]
307pub enum BlocklistType {
308    Blocklist = 0,
309    Unblocklist = 1,
310}
311
312#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
313pub struct BlocklistCommitteeAction {
314    pub nonce: u64,
315    pub chain_id: BridgeChainId,
316    pub blocklist_type: BlocklistType,
317    pub members_to_update: Vec<BridgeAuthorityPublicKeyBytes>,
318}
319
320#[derive(
321    Debug,
322    Serialize,
323    Deserialize,
324    PartialEq,
325    Eq,
326    Clone,
327    Copy,
328    TryFromPrimitive,
329    Hash,
330    clap::ValueEnum,
331)]
332#[repr(u8)]
333pub enum EmergencyActionType {
334    Pause = 0,
335    Unpause = 1,
336}
337
338#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
339pub struct EmergencyAction {
340    pub nonce: u64,
341    pub chain_id: BridgeChainId,
342    pub action_type: EmergencyActionType,
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
346pub struct LimitUpdateAction {
347    pub nonce: u64,
348    // The chain id that will receive this signed action. It's also the destination chain id
349    // for the limit update. For example, if chain_id is EthMainnet and sending_chain_id is SuiMainnet,
350    // it means we want to update the limit for the SuiMainnet to EthMainnet route.
351    pub chain_id: BridgeChainId,
352    // The sending chain id for the limit update.
353    pub sending_chain_id: BridgeChainId,
354    // 4 decimal places, namely 1 USD = 10000
355    pub new_usd_limit: u64,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
359pub struct AssetPriceUpdateAction {
360    pub nonce: u64,
361    pub chain_id: BridgeChainId,
362    pub token_id: u8,
363    pub new_usd_price: u64,
364}
365
366#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
367pub struct EvmContractUpgradeAction {
368    pub nonce: u64,
369    pub chain_id: BridgeChainId,
370    pub proxy_address: EthAddress,
371    pub new_impl_address: EthAddress,
372    pub call_data: Vec<u8>,
373}
374
375#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
376pub struct AddTokensOnSuiAction {
377    pub nonce: u64,
378    pub chain_id: BridgeChainId,
379    pub native: bool,
380    pub token_ids: Vec<u8>,
381    pub token_type_names: Vec<TypeTag>,
382    pub token_prices: Vec<u64>,
383}
384
385#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
386pub struct AddTokensOnEvmAction {
387    pub nonce: u64,
388    pub chain_id: BridgeChainId,
389    pub native: bool,
390    pub token_ids: Vec<u8>,
391    pub token_addresses: Vec<EthAddress>,
392    pub token_sui_decimals: Vec<u8>,
393    pub token_prices: Vec<u64>,
394}
395
396/// The type of actions Bridge Committee verify and sign off to execution.
397/// Its relationship with BridgeEvent is similar to the relationship between
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
399#[enum_dispatch(BridgeMessageEncoding)]
400pub enum BridgeAction {
401    /// Sui to Eth bridge action
402    SuiToEthBridgeAction(SuiToEthBridgeAction),
403    /// Eth to sui bridge action
404    EthToSuiBridgeAction(EthToSuiBridgeAction),
405    BlocklistCommitteeAction(BlocklistCommitteeAction),
406    EmergencyAction(EmergencyAction),
407    LimitUpdateAction(LimitUpdateAction),
408    AssetPriceUpdateAction(AssetPriceUpdateAction),
409    EvmContractUpgradeAction(EvmContractUpgradeAction),
410    AddTokensOnSuiAction(AddTokensOnSuiAction),
411    AddTokensOnEvmAction(AddTokensOnEvmAction),
412    /// Sui to Eth bridge action
413    SuiToEthTokenTransfer(SuiToEthTokenTransfer),
414    /// Sui to Eth bridge action V2
415    SuiToEthTokenTransferV2(SuiToEthTokenTransferV2),
416    // /// Eth to sui bridge action V2
417    EthToSuiTokenTransferV2(EthToSuiTokenTransferV2),
418}
419
420impl BridgeAction {
421    // Digest of BridgeAction (with Keccak256 hasher)
422    pub fn digest(&self) -> BridgeActionDigest {
423        let mut hasher = Keccak256::default();
424        hasher.update(
425            self.to_bytes()
426                .expect("Message encoding should not fail for valid actions"),
427        );
428        BridgeActionDigest::new(hasher.finalize().into())
429    }
430
431    pub fn key(&self) -> BridgeActionKey {
432        BridgeActionKey {
433            action_type: self.action_type(),
434            chain_id: self.chain_id(),
435            seq_num: self.seq_number(),
436        }
437    }
438
439    pub fn chain_id(&self) -> BridgeChainId {
440        match self {
441            BridgeAction::SuiToEthBridgeAction(a) => a.sui_bridge_event.sui_chain_id,
442            BridgeAction::SuiToEthTokenTransfer(a) => a.sui_chain_id,
443            BridgeAction::SuiToEthTokenTransferV2(a) => a.sui_chain_id,
444            BridgeAction::EthToSuiBridgeAction(a) => a.eth_bridge_event.eth_chain_id,
445            BridgeAction::EthToSuiTokenTransferV2(a) => a.eth_bridge_event.eth_chain_id,
446            BridgeAction::BlocklistCommitteeAction(a) => a.chain_id,
447            BridgeAction::EmergencyAction(a) => a.chain_id,
448            BridgeAction::LimitUpdateAction(a) => a.chain_id,
449            BridgeAction::AssetPriceUpdateAction(a) => a.chain_id,
450            BridgeAction::EvmContractUpgradeAction(a) => a.chain_id,
451            BridgeAction::AddTokensOnSuiAction(a) => a.chain_id,
452            BridgeAction::AddTokensOnEvmAction(a) => a.chain_id,
453        }
454    }
455
456    pub fn is_governance_action(&self) -> bool {
457        match self.action_type() {
458            BridgeActionType::TokenTransfer => false,
459            BridgeActionType::UpdateCommitteeBlocklist => true,
460            BridgeActionType::EmergencyButton => true,
461            BridgeActionType::LimitUpdate => true,
462            BridgeActionType::AssetPriceUpdate => true,
463            BridgeActionType::EvmContractUpgrade => true,
464            BridgeActionType::AddTokensOnSui => true,
465            BridgeActionType::AddTokensOnEvm => true,
466        }
467    }
468
469    // Also called `message_type`
470    pub fn action_type(&self) -> BridgeActionType {
471        match self {
472            BridgeAction::SuiToEthBridgeAction(_) => BridgeActionType::TokenTransfer,
473            BridgeAction::SuiToEthTokenTransfer(_) => BridgeActionType::TokenTransfer,
474            BridgeAction::SuiToEthTokenTransferV2(_) => BridgeActionType::TokenTransfer,
475            BridgeAction::EthToSuiBridgeAction(_) => BridgeActionType::TokenTransfer,
476            BridgeAction::EthToSuiTokenTransferV2(_) => BridgeActionType::TokenTransfer,
477            BridgeAction::BlocklistCommitteeAction(_) => BridgeActionType::UpdateCommitteeBlocklist,
478            BridgeAction::EmergencyAction(_) => BridgeActionType::EmergencyButton,
479            BridgeAction::LimitUpdateAction(_) => BridgeActionType::LimitUpdate,
480            BridgeAction::AssetPriceUpdateAction(_) => BridgeActionType::AssetPriceUpdate,
481            BridgeAction::EvmContractUpgradeAction(_) => BridgeActionType::EvmContractUpgrade,
482            BridgeAction::AddTokensOnSuiAction(_) => BridgeActionType::AddTokensOnSui,
483            BridgeAction::AddTokensOnEvmAction(_) => BridgeActionType::AddTokensOnEvm,
484        }
485    }
486
487    // Also called `nonce`
488    pub fn seq_number(&self) -> u64 {
489        match self {
490            BridgeAction::SuiToEthBridgeAction(a) => a.sui_bridge_event.nonce,
491            BridgeAction::SuiToEthTokenTransfer(a) => a.nonce,
492            BridgeAction::SuiToEthTokenTransferV2(a) => a.nonce,
493            BridgeAction::EthToSuiBridgeAction(a) => a.eth_bridge_event.nonce,
494            BridgeAction::EthToSuiTokenTransferV2(a) => a.eth_bridge_event.nonce,
495            BridgeAction::BlocklistCommitteeAction(a) => a.nonce,
496            BridgeAction::EmergencyAction(a) => a.nonce,
497            BridgeAction::LimitUpdateAction(a) => a.nonce,
498            BridgeAction::AssetPriceUpdateAction(a) => a.nonce,
499            BridgeAction::EvmContractUpgradeAction(a) => a.nonce,
500            BridgeAction::AddTokensOnSuiAction(a) => a.nonce,
501            BridgeAction::AddTokensOnEvmAction(a) => a.nonce,
502        }
503    }
504
505    pub fn approval_threshold(&self) -> u64 {
506        match self {
507            BridgeAction::SuiToEthBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
508            BridgeAction::SuiToEthTokenTransfer(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
509            BridgeAction::SuiToEthTokenTransferV2(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
510            BridgeAction::EthToSuiBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
511            BridgeAction::EthToSuiTokenTransferV2(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
512            BridgeAction::BlocklistCommitteeAction(_) => APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST,
513            BridgeAction::EmergencyAction(a) => match a.action_type {
514                EmergencyActionType::Pause => APPROVAL_THRESHOLD_EMERGENCY_PAUSE,
515                EmergencyActionType::Unpause => APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE,
516            },
517            BridgeAction::LimitUpdateAction(_) => APPROVAL_THRESHOLD_LIMIT_UPDATE,
518            BridgeAction::AssetPriceUpdateAction(_) => APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE,
519            BridgeAction::EvmContractUpgradeAction(_) => APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE,
520            BridgeAction::AddTokensOnSuiAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI,
521            BridgeAction::AddTokensOnEvmAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM,
522        }
523    }
524
525    // Update the action to the new TokenTransfer variant that requests signing via the new route
526    pub fn update_to_token_transfer(self) -> Self {
527        match self {
528            BridgeAction::SuiToEthBridgeAction(a) => {
529                BridgeAction::SuiToEthTokenTransfer(SuiToEthTokenTransfer {
530                    nonce: a.sui_bridge_event.nonce,
531                    sui_chain_id: a.sui_bridge_event.sui_chain_id,
532                    eth_chain_id: a.sui_bridge_event.eth_chain_id,
533                    sui_address: a.sui_bridge_event.sui_address,
534                    eth_address: a.sui_bridge_event.eth_address,
535                    token_id: a.sui_bridge_event.token_id,
536                    amount_adjusted: a.sui_bridge_event.amount_sui_adjusted,
537                })
538            }
539            BridgeAction::EthToSuiBridgeAction(_) => self,
540            BridgeAction::EthToSuiTokenTransferV2(_) => self,
541            BridgeAction::BlocklistCommitteeAction(_) => self,
542            BridgeAction::EmergencyAction(_) => self,
543            BridgeAction::LimitUpdateAction(_) => self,
544            BridgeAction::AssetPriceUpdateAction(_) => self,
545            BridgeAction::EvmContractUpgradeAction(_) => self,
546            BridgeAction::AddTokensOnSuiAction(_) => self,
547            BridgeAction::AddTokensOnEvmAction(_) => self,
548            BridgeAction::SuiToEthTokenTransfer(_) => self,
549            BridgeAction::SuiToEthTokenTransferV2(_) => self,
550        }
551    }
552
553    pub fn try_from_bridge_record(record: &MoveTypeBridgeRecord) -> BridgeResult<Self> {
554        use std::str::FromStr;
555
556        let MoveTypeBridgeMessage {
557            message_type: _,
558            message_version,
559            seq_num,
560            source_chain,
561            payload,
562        } = &record.message;
563
564        #[derive(Debug, Deserialize)]
565        struct SuiToEthOnChainBcsPayload {
566            sui_address: Vec<u8>,
567            target_chain: u8,
568            eth_address: Vec<u8>,
569            token_type: u8,
570            amount: [u8; 8], // u64 as Big Endian bytes
571        }
572
573        #[derive(Debug, Deserialize)]
574        struct SuiToEthOnChainBcsPayloadV2 {
575            sui_address: Vec<u8>,
576            target_chain: u8,
577            eth_address: Vec<u8>,
578            token_type: u8,
579            amount: [u8; 8],       // u64 as Big Endian bytes
580            timestamp_ms: [u8; 8], // u64 as Big Endian bytes
581        }
582
583        match *message_version {
584            crate::encoding::TOKEN_TRANSFER_MESSAGE_VERSION_V1 => {
585                let payload: SuiToEthOnChainBcsPayload = bcs::from_bytes(payload)?;
586
587                Ok(BridgeAction::SuiToEthTokenTransfer(SuiToEthTokenTransfer {
588                    nonce: *seq_num,
589                    sui_chain_id: BridgeChainId::try_from(*source_chain)?,
590                    eth_chain_id: BridgeChainId::try_from(payload.target_chain)?,
591                    sui_address: SuiAddress::from_bytes(payload.sui_address)?,
592                    eth_address: EthAddress::from_str(&Hex::encode(&payload.eth_address))?,
593                    token_id: payload.token_type,
594                    amount_adjusted: u64::from_be_bytes(payload.amount),
595                }))
596            }
597            crate::encoding::TOKEN_TRANSFER_MESSAGE_VERSION_V2 => {
598                let payload: SuiToEthOnChainBcsPayloadV2 = bcs::from_bytes(payload)?;
599
600                Ok(BridgeAction::SuiToEthTokenTransferV2(
601                    SuiToEthTokenTransferV2 {
602                        nonce: *seq_num,
603                        sui_chain_id: BridgeChainId::try_from(*source_chain)?,
604                        eth_chain_id: BridgeChainId::try_from(payload.target_chain)?,
605                        sui_address: SuiAddress::from_bytes(payload.sui_address)?,
606                        eth_address: EthAddress::from_str(&Hex::encode(&payload.eth_address))?,
607                        token_id: payload.token_type,
608                        amount_adjusted: u64::from_be_bytes(payload.amount),
609                        timestamp_ms: u64::from_be_bytes(payload.timestamp_ms),
610                    },
611                ))
612            }
613            v => Err(BridgeError::Generic(format!(
614                "unknown message version: {v}"
615            ))),
616        }
617    }
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
621pub struct BridgeActionDigest(Digest);
622
623impl BridgeActionDigest {
624    pub const fn new(digest: [u8; 32]) -> Self {
625        Self(Digest::new(digest))
626    }
627}
628
629#[derive(Debug, Clone)]
630pub struct BridgeCommitteeValiditySignInfo {
631    pub signatures: BTreeMap<BridgeAuthorityPublicKeyBytes, BridgeAuthorityRecoverableSignature>,
632}
633
634pub type SignedBridgeAction = Envelope<BridgeAction, BridgeAuthoritySignInfo>;
635pub type VerifiedSignedBridgeAction = VerifiedEnvelope<BridgeAction, BridgeAuthoritySignInfo>;
636pub type CertifiedBridgeAction = Envelope<BridgeAction, BridgeCommitteeValiditySignInfo>;
637pub type VerifiedCertifiedBridgeAction =
638    VerifiedEnvelope<BridgeAction, BridgeCommitteeValiditySignInfo>;
639
640#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
641pub struct BridgeEventDigest(Digest);
642
643impl BridgeEventDigest {
644    pub const fn new(digest: [u8; 32]) -> Self {
645        Self(Digest::new(digest))
646    }
647}
648
649impl Message for BridgeAction {
650    type DigestType = BridgeEventDigest;
651
652    // this is not encoded in message today
653    const SCOPE: IntentScope = IntentScope::BridgeEventUnused;
654
655    // this is not used today
656    fn digest(&self) -> Self::DigestType {
657        unreachable!("BridgeEventDigest is not used today")
658    }
659}
660
661#[derive(Debug, Clone, PartialEq, Eq)]
662pub struct EthLog {
663    pub block_number: u64,
664    pub tx_hash: B256,
665    pub log_index_in_tx: u16,
666    pub log: Log,
667}
668
669/// The version of EthLog that does not have
670/// `log_index_in_tx`.
671#[derive(Debug, Clone, PartialEq, Eq)]
672pub struct RawEthLog {
673    pub block_number: u64,
674    pub tx_hash: B256,
675    pub log: Log,
676}
677
678pub trait EthEvent {
679    fn block_number(&self) -> u64;
680    fn tx_hash(&self) -> B256;
681    fn log(&self) -> &Log;
682}
683
684impl EthEvent for EthLog {
685    fn block_number(&self) -> u64 {
686        self.block_number
687    }
688    fn tx_hash(&self) -> B256 {
689        self.tx_hash
690    }
691    fn log(&self) -> &Log {
692        &self.log
693    }
694}
695
696impl EthEvent for RawEthLog {
697    fn block_number(&self) -> u64 {
698        self.block_number
699    }
700    fn tx_hash(&self) -> B256 {
701        self.tx_hash
702    }
703    fn log(&self) -> &Log {
704        &self.log
705    }
706}
707
708/// Check if the bridge route is valid
709/// Only mainnet can bridge to mainnet, other than that we do not care.
710pub fn is_route_valid(one: BridgeChainId, other: BridgeChainId) -> bool {
711    if one.is_sui_chain() && other.is_sui_chain() {
712        return false;
713    }
714    if !one.is_sui_chain() && !other.is_sui_chain() {
715        return false;
716    }
717    if one == BridgeChainId::EthMainnet {
718        return other == BridgeChainId::SuiMainnet;
719    }
720    if one == BridgeChainId::SuiMainnet {
721        return other == BridgeChainId::EthMainnet;
722    }
723    if other == BridgeChainId::EthMainnet {
724        return one == BridgeChainId::SuiMainnet;
725    }
726    if other == BridgeChainId::SuiMainnet {
727        return one == BridgeChainId::EthMainnet;
728    }
729    true
730}
731
732// Sanitized version of MoveTypeParsedTokenTransferMessage
733#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
734pub struct ParsedTokenTransferMessage {
735    pub message_version: u8,
736    pub seq_num: u64,
737    pub source_chain: BridgeChainId,
738    pub payload: Vec<u8>,
739    pub parsed_payload: MoveTypeTokenTransferPayload,
740}
741
742impl TryFrom<MoveTypeParsedTokenTransferMessage> for ParsedTokenTransferMessage {
743    type Error = BridgeError;
744
745    fn try_from(message: MoveTypeParsedTokenTransferMessage) -> BridgeResult<Self> {
746        let source_chain = BridgeChainId::try_from(message.source_chain).map_err(|_e| {
747            BridgeError::Generic(format!(
748                "Failed to convert MoveTypeParsedTokenTransferMessage to ParsedTokenTransferMessage. Failed to convert source chain {} to BridgeChainId",
749                message.source_chain,
750            ))
751        })?;
752        Ok(Self {
753            message_version: message.message_version,
754            seq_num: message.seq_num,
755            source_chain,
756            payload: message.payload,
757            parsed_payload: message.parsed_payload,
758        })
759    }
760}
761
762pub struct SuiEvents {
763    pub transaction_digest: TransactionDigest,
764    pub checkpoint: Option<u64>,
765    pub timestamp_ms: Option<u64>,
766    pub events: Vec<sui_json_rpc_types::SuiEvent>,
767}
768
769#[cfg(test)]
770mod tests {
771    use crate::test_utils::get_test_authority_and_key;
772    use crate::test_utils::get_test_eth_to_sui_bridge_action;
773    use crate::test_utils::get_test_sui_to_eth_bridge_action;
774    use alloy::primitives::Address as EthAddress;
775    use fastcrypto::traits::KeyPair;
776    use std::collections::HashSet;
777    use sui_types::bridge::TOKEN_ID_BTC;
778    use sui_types::crypto::get_key_pair;
779
780    use super::*;
781
782    #[test]
783    fn test_bridge_committee_construction() -> anyhow::Result<()> {
784        let (mut authority, _, _) = get_test_authority_and_key(8000, 9999);
785        // This is ok
786        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap();
787
788        // This is not ok - total voting power < BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER
789        authority.voting_power = BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER - 1;
790        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err();
791
792        // This is not ok - total voting power > BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER
793        authority.voting_power = BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER + 1;
794        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err();
795
796        // This is ok
797        authority.voting_power = 5000;
798        let mut authority_2 = authority.clone();
799        let (_, kp): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair();
800        let pubkey = kp.public().clone();
801        authority_2.pubkey = pubkey.clone();
802        let _ = BridgeCommittee::new(vec![authority.clone(), authority_2.clone()]).unwrap();
803
804        // This is not ok - duplicate pub key
805        authority_2.pubkey = authority.pubkey.clone();
806        let _ = BridgeCommittee::new(vec![authority.clone(), authority.clone()]).unwrap_err();
807        Ok(())
808    }
809
810    #[test]
811    fn test_bridge_committee_total_blocklisted_stake() -> anyhow::Result<()> {
812        let (mut authority1, _, _) = get_test_authority_and_key(10000, 9999);
813        assert_eq!(
814            BridgeCommittee::new(vec![authority1.clone()])
815                .unwrap()
816                .total_blocklisted_stake(),
817            0
818        );
819        authority1.voting_power = 6000;
820
821        let (mut authority2, _, _) = get_test_authority_and_key(4000, 9999);
822        authority2.is_blocklisted = true;
823        assert_eq!(
824            BridgeCommittee::new(vec![authority1.clone(), authority2.clone()])
825                .unwrap()
826                .total_blocklisted_stake(),
827            4000
828        );
829
830        authority1.voting_power = 7000;
831        authority2.voting_power = 2000;
832        let (mut authority3, _, _) = get_test_authority_and_key(1000, 9999);
833        authority3.is_blocklisted = true;
834        assert_eq!(
835            BridgeCommittee::new(vec![authority1, authority2, authority3])
836                .unwrap()
837                .total_blocklisted_stake(),
838            3000
839        );
840
841        Ok(())
842    }
843
844    // Regression test to avoid accidentally change to approval threshold
845    #[test]
846    fn test_bridge_action_approval_threshold_regression_test() -> anyhow::Result<()> {
847        let action = get_test_sui_to_eth_bridge_action(None, None, None, None, None, None, None);
848        assert_eq!(action.approval_threshold(), 3334);
849
850        let action = get_test_eth_to_sui_bridge_action(None, None, None, None);
851        assert_eq!(action.approval_threshold(), 3334);
852
853        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
854            nonce: 94,
855            chain_id: BridgeChainId::EthSepolia,
856            blocklist_type: BlocklistType::Unblocklist,
857            members_to_update: vec![],
858        });
859        assert_eq!(action.approval_threshold(), 5001);
860
861        let action = BridgeAction::EmergencyAction(EmergencyAction {
862            nonce: 56,
863            chain_id: BridgeChainId::EthSepolia,
864            action_type: EmergencyActionType::Pause,
865        });
866        assert_eq!(action.approval_threshold(), 450);
867
868        let action = BridgeAction::EmergencyAction(EmergencyAction {
869            nonce: 56,
870            chain_id: BridgeChainId::EthSepolia,
871            action_type: EmergencyActionType::Unpause,
872        });
873        assert_eq!(action.approval_threshold(), 5001);
874
875        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
876            nonce: 15,
877            chain_id: BridgeChainId::SuiCustom,
878            sending_chain_id: BridgeChainId::EthCustom,
879            new_usd_limit: 1_000_000 * USD_MULTIPLIER,
880        });
881        assert_eq!(action.approval_threshold(), 5001);
882
883        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
884            nonce: 266,
885            chain_id: BridgeChainId::SuiCustom,
886            token_id: TOKEN_ID_BTC,
887            new_usd_price: 100_000 * USD_MULTIPLIER,
888        });
889        assert_eq!(action.approval_threshold(), 5001);
890
891        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
892            nonce: 123,
893            chain_id: BridgeChainId::EthCustom,
894            proxy_address: EthAddress::repeat_byte(6),
895            new_impl_address: EthAddress::repeat_byte(9),
896            call_data: vec![],
897        });
898        assert_eq!(action.approval_threshold(), 5001);
899        Ok(())
900    }
901
902    #[test]
903    fn test_bridge_committee_filter_blocklisted_authorities() -> anyhow::Result<()> {
904        // Note: today BridgeCommittee does not shuffle authorities
905        let (authority1, _, _) = get_test_authority_and_key(5000, 9999);
906        let (mut authority2, _, _) = get_test_authority_and_key(3000, 9999);
907        authority2.is_blocklisted = true;
908        let (authority3, _, _) = get_test_authority_and_key(2000, 9999);
909        let committee = BridgeCommittee::new(vec![
910            authority1.clone(),
911            authority2.clone(),
912            authority3.clone(),
913        ])
914        .unwrap();
915
916        // exclude authority2
917        let result = committee
918            .shuffle_by_stake(None, None)
919            .into_iter()
920            .collect::<HashSet<_>>();
921        assert_eq!(
922            HashSet::from_iter(vec![authority1.pubkey_bytes(), authority3.pubkey_bytes()]),
923            result
924        );
925
926        // exclude authority2 and authority3
927        let result = committee
928            .shuffle_by_stake(
929                None,
930                Some(
931                    &[authority1.pubkey_bytes(), authority2.pubkey_bytes()]
932                        .iter()
933                        .cloned()
934                        .collect(),
935                ),
936            )
937            .into_iter()
938            .collect::<HashSet<_>>();
939        assert_eq!(HashSet::from_iter(vec![authority1.pubkey_bytes()]), result);
940
941        Ok(())
942    }
943}