sui_bridge/
types.rs

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