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    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 action_type: BridgeActionType,
221    pub chain_id: BridgeChainId,
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 EthToSuiBridgeAction {
255    // Digest of the transaction where the event was emitted
256    pub eth_tx_hash: EthTransactionHash,
257    // The index of the event in the transaction
258    pub eth_event_index: u16,
259    pub eth_bridge_event: EthToSuiTokenBridgeV1,
260}
261
262#[derive(
263    Debug,
264    Serialize,
265    Deserialize,
266    PartialEq,
267    Eq,
268    Clone,
269    Copy,
270    TryFromPrimitive,
271    Hash,
272    clap::ValueEnum,
273)]
274#[repr(u8)]
275pub enum BlocklistType {
276    Blocklist = 0,
277    Unblocklist = 1,
278}
279
280#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
281pub struct BlocklistCommitteeAction {
282    pub nonce: u64,
283    pub chain_id: BridgeChainId,
284    pub blocklist_type: BlocklistType,
285    pub members_to_update: Vec<BridgeAuthorityPublicKeyBytes>,
286}
287
288#[derive(
289    Debug,
290    Serialize,
291    Deserialize,
292    PartialEq,
293    Eq,
294    Clone,
295    Copy,
296    TryFromPrimitive,
297    Hash,
298    clap::ValueEnum,
299)]
300#[repr(u8)]
301pub enum EmergencyActionType {
302    Pause = 0,
303    Unpause = 1,
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
307pub struct EmergencyAction {
308    pub nonce: u64,
309    pub chain_id: BridgeChainId,
310    pub action_type: EmergencyActionType,
311}
312
313#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
314pub struct LimitUpdateAction {
315    pub nonce: u64,
316    // The chain id that will receive this signed action. It's also the destination chain id
317    // for the limit update. For example, if chain_id is EthMainnet and sending_chain_id is SuiMainnet,
318    // it means we want to update the limit for the SuiMainnet to EthMainnet route.
319    pub chain_id: BridgeChainId,
320    // The sending chain id for the limit update.
321    pub sending_chain_id: BridgeChainId,
322    // 4 decimal places, namely 1 USD = 10000
323    pub new_usd_limit: u64,
324}
325
326#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
327pub struct AssetPriceUpdateAction {
328    pub nonce: u64,
329    pub chain_id: BridgeChainId,
330    pub token_id: u8,
331    pub new_usd_price: u64,
332}
333
334#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
335pub struct EvmContractUpgradeAction {
336    pub nonce: u64,
337    pub chain_id: BridgeChainId,
338    pub proxy_address: EthAddress,
339    pub new_impl_address: EthAddress,
340    pub call_data: Vec<u8>,
341}
342
343#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
344pub struct AddTokensOnSuiAction {
345    pub nonce: u64,
346    pub chain_id: BridgeChainId,
347    pub native: bool,
348    pub token_ids: Vec<u8>,
349    pub token_type_names: Vec<TypeTag>,
350    pub token_prices: Vec<u64>,
351}
352
353#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
354pub struct AddTokensOnEvmAction {
355    pub nonce: u64,
356    pub chain_id: BridgeChainId,
357    pub native: bool,
358    pub token_ids: Vec<u8>,
359    pub token_addresses: Vec<EthAddress>,
360    pub token_sui_decimals: Vec<u8>,
361    pub token_prices: Vec<u64>,
362}
363
364/// The type of actions Bridge Committee verify and sign off to execution.
365/// Its relationship with BridgeEvent is similar to the relationship between
366#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
367#[enum_dispatch(BridgeMessageEncoding)]
368pub enum BridgeAction {
369    /// Sui to Eth bridge action
370    SuiToEthBridgeAction(SuiToEthBridgeAction),
371    /// Eth to sui bridge action
372    EthToSuiBridgeAction(EthToSuiBridgeAction),
373    BlocklistCommitteeAction(BlocklistCommitteeAction),
374    EmergencyAction(EmergencyAction),
375    LimitUpdateAction(LimitUpdateAction),
376    AssetPriceUpdateAction(AssetPriceUpdateAction),
377    EvmContractUpgradeAction(EvmContractUpgradeAction),
378    AddTokensOnSuiAction(AddTokensOnSuiAction),
379    AddTokensOnEvmAction(AddTokensOnEvmAction),
380}
381
382impl BridgeAction {
383    // Digest of BridgeAction (with Keccak256 hasher)
384    pub fn digest(&self) -> BridgeActionDigest {
385        let mut hasher = Keccak256::default();
386        hasher.update(
387            self.to_bytes()
388                .expect("Message encoding should not fail for valid actions"),
389        );
390        BridgeActionDigest::new(hasher.finalize().into())
391    }
392
393    pub fn key(&self) -> BridgeActionKey {
394        BridgeActionKey {
395            action_type: self.action_type(),
396            chain_id: self.chain_id(),
397            seq_num: self.seq_number(),
398        }
399    }
400
401    pub fn chain_id(&self) -> BridgeChainId {
402        match self {
403            BridgeAction::SuiToEthBridgeAction(a) => a.sui_bridge_event.sui_chain_id,
404            BridgeAction::EthToSuiBridgeAction(a) => a.eth_bridge_event.eth_chain_id,
405            BridgeAction::BlocklistCommitteeAction(a) => a.chain_id,
406            BridgeAction::EmergencyAction(a) => a.chain_id,
407            BridgeAction::LimitUpdateAction(a) => a.chain_id,
408            BridgeAction::AssetPriceUpdateAction(a) => a.chain_id,
409            BridgeAction::EvmContractUpgradeAction(a) => a.chain_id,
410            BridgeAction::AddTokensOnSuiAction(a) => a.chain_id,
411            BridgeAction::AddTokensOnEvmAction(a) => a.chain_id,
412        }
413    }
414
415    pub fn is_governace_action(&self) -> bool {
416        match self.action_type() {
417            BridgeActionType::TokenTransfer => false,
418            BridgeActionType::UpdateCommitteeBlocklist => true,
419            BridgeActionType::EmergencyButton => true,
420            BridgeActionType::LimitUpdate => true,
421            BridgeActionType::AssetPriceUpdate => true,
422            BridgeActionType::EvmContractUpgrade => true,
423            BridgeActionType::AddTokensOnSui => true,
424            BridgeActionType::AddTokensOnEvm => true,
425        }
426    }
427
428    // Also called `message_type`
429    pub fn action_type(&self) -> BridgeActionType {
430        match self {
431            BridgeAction::SuiToEthBridgeAction(_) => BridgeActionType::TokenTransfer,
432            BridgeAction::EthToSuiBridgeAction(_) => BridgeActionType::TokenTransfer,
433            BridgeAction::BlocklistCommitteeAction(_) => BridgeActionType::UpdateCommitteeBlocklist,
434            BridgeAction::EmergencyAction(_) => BridgeActionType::EmergencyButton,
435            BridgeAction::LimitUpdateAction(_) => BridgeActionType::LimitUpdate,
436            BridgeAction::AssetPriceUpdateAction(_) => BridgeActionType::AssetPriceUpdate,
437            BridgeAction::EvmContractUpgradeAction(_) => BridgeActionType::EvmContractUpgrade,
438            BridgeAction::AddTokensOnSuiAction(_) => BridgeActionType::AddTokensOnSui,
439            BridgeAction::AddTokensOnEvmAction(_) => BridgeActionType::AddTokensOnEvm,
440        }
441    }
442
443    // Also called `nonce`
444    pub fn seq_number(&self) -> u64 {
445        match self {
446            BridgeAction::SuiToEthBridgeAction(a) => a.sui_bridge_event.nonce,
447            BridgeAction::EthToSuiBridgeAction(a) => a.eth_bridge_event.nonce,
448            BridgeAction::BlocklistCommitteeAction(a) => a.nonce,
449            BridgeAction::EmergencyAction(a) => a.nonce,
450            BridgeAction::LimitUpdateAction(a) => a.nonce,
451            BridgeAction::AssetPriceUpdateAction(a) => a.nonce,
452            BridgeAction::EvmContractUpgradeAction(a) => a.nonce,
453            BridgeAction::AddTokensOnSuiAction(a) => a.nonce,
454            BridgeAction::AddTokensOnEvmAction(a) => a.nonce,
455        }
456    }
457
458    pub fn approval_threshold(&self) -> u64 {
459        match self {
460            BridgeAction::SuiToEthBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
461            BridgeAction::EthToSuiBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
462            BridgeAction::BlocklistCommitteeAction(_) => APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST,
463            BridgeAction::EmergencyAction(a) => match a.action_type {
464                EmergencyActionType::Pause => APPROVAL_THRESHOLD_EMERGENCY_PAUSE,
465                EmergencyActionType::Unpause => APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE,
466            },
467            BridgeAction::LimitUpdateAction(_) => APPROVAL_THRESHOLD_LIMIT_UPDATE,
468            BridgeAction::AssetPriceUpdateAction(_) => APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE,
469            BridgeAction::EvmContractUpgradeAction(_) => APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE,
470            BridgeAction::AddTokensOnSuiAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI,
471            BridgeAction::AddTokensOnEvmAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM,
472        }
473    }
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
477pub struct BridgeActionDigest(Digest);
478
479impl BridgeActionDigest {
480    pub const fn new(digest: [u8; 32]) -> Self {
481        Self(Digest::new(digest))
482    }
483}
484
485#[derive(Debug, Clone)]
486pub struct BridgeCommitteeValiditySignInfo {
487    pub signatures: BTreeMap<BridgeAuthorityPublicKeyBytes, BridgeAuthorityRecoverableSignature>,
488}
489
490pub type SignedBridgeAction = Envelope<BridgeAction, BridgeAuthoritySignInfo>;
491pub type VerifiedSignedBridgeAction = VerifiedEnvelope<BridgeAction, BridgeAuthoritySignInfo>;
492pub type CertifiedBridgeAction = Envelope<BridgeAction, BridgeCommitteeValiditySignInfo>;
493pub type VerifiedCertifiedBridgeAction =
494    VerifiedEnvelope<BridgeAction, BridgeCommitteeValiditySignInfo>;
495
496#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
497pub struct BridgeEventDigest(Digest);
498
499impl BridgeEventDigest {
500    pub const fn new(digest: [u8; 32]) -> Self {
501        Self(Digest::new(digest))
502    }
503}
504
505impl Message for BridgeAction {
506    type DigestType = BridgeEventDigest;
507
508    // this is not encoded in message today
509    const SCOPE: IntentScope = IntentScope::BridgeEventUnused;
510
511    // this is not used today
512    fn digest(&self) -> Self::DigestType {
513        unreachable!("BridgeEventDigest is not used today")
514    }
515}
516
517#[derive(Debug, Clone, PartialEq, Eq)]
518pub struct EthLog {
519    pub block_number: u64,
520    pub tx_hash: H256,
521    pub log_index_in_tx: u16,
522    pub log: Log,
523}
524
525/// The version of EthLog that does not have
526/// `log_index_in_tx`.
527#[derive(Debug, Clone, PartialEq, Eq)]
528pub struct RawEthLog {
529    pub block_number: u64,
530    pub tx_hash: H256,
531    pub log: Log,
532}
533
534pub trait EthEvent {
535    fn block_number(&self) -> u64;
536    fn tx_hash(&self) -> H256;
537    fn log(&self) -> &Log;
538}
539
540impl EthEvent for EthLog {
541    fn block_number(&self) -> u64 {
542        self.block_number
543    }
544    fn tx_hash(&self) -> H256 {
545        self.tx_hash
546    }
547    fn log(&self) -> &Log {
548        &self.log
549    }
550}
551
552impl EthEvent for RawEthLog {
553    fn block_number(&self) -> u64 {
554        self.block_number
555    }
556    fn tx_hash(&self) -> H256 {
557        self.tx_hash
558    }
559    fn log(&self) -> &Log {
560        &self.log
561    }
562}
563
564/// Check if the bridge route is valid
565/// Only mainnet can bridge to mainnet, other than that we do not care.
566pub fn is_route_valid(one: BridgeChainId, other: BridgeChainId) -> bool {
567    if one.is_sui_chain() && other.is_sui_chain() {
568        return false;
569    }
570    if !one.is_sui_chain() && !other.is_sui_chain() {
571        return false;
572    }
573    if one == BridgeChainId::EthMainnet {
574        return other == BridgeChainId::SuiMainnet;
575    }
576    if one == BridgeChainId::SuiMainnet {
577        return other == BridgeChainId::EthMainnet;
578    }
579    if other == BridgeChainId::EthMainnet {
580        return one == BridgeChainId::SuiMainnet;
581    }
582    if other == BridgeChainId::SuiMainnet {
583        return one == BridgeChainId::EthMainnet;
584    }
585    true
586}
587
588// Sanitized version of MoveTypeParsedTokenTransferMessage
589#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
590pub struct ParsedTokenTransferMessage {
591    pub message_version: u8,
592    pub seq_num: u64,
593    pub source_chain: BridgeChainId,
594    pub payload: Vec<u8>,
595    pub parsed_payload: MoveTypeTokenTransferPayload,
596}
597
598impl TryFrom<MoveTypeParsedTokenTransferMessage> for ParsedTokenTransferMessage {
599    type Error = BridgeError;
600
601    fn try_from(message: MoveTypeParsedTokenTransferMessage) -> BridgeResult<Self> {
602        let source_chain = BridgeChainId::try_from(message.source_chain).map_err(|_e| {
603            BridgeError::Generic(format!(
604                "Failed to convert MoveTypeParsedTokenTransferMessage to ParsedTokenTransferMessage. Failed to convert source chain {} to BridgeChainId",
605                message.source_chain,
606            ))
607        })?;
608        Ok(Self {
609            message_version: message.message_version,
610            seq_num: message.seq_num,
611            source_chain,
612            payload: message.payload,
613            parsed_payload: message.parsed_payload,
614        })
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use crate::test_utils::get_test_authority_and_key;
621    use crate::test_utils::get_test_eth_to_sui_bridge_action;
622    use crate::test_utils::get_test_sui_to_eth_bridge_action;
623    use ethers::types::Address as EthAddress;
624    use fastcrypto::traits::KeyPair;
625    use std::collections::HashSet;
626    use sui_types::bridge::TOKEN_ID_BTC;
627    use sui_types::crypto::get_key_pair;
628
629    use super::*;
630
631    #[test]
632    fn test_bridge_committee_construction() -> anyhow::Result<()> {
633        let (mut authority, _, _) = get_test_authority_and_key(8000, 9999);
634        // This is ok
635        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap();
636
637        // This is not ok - total voting power < BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER
638        authority.voting_power = BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER - 1;
639        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err();
640
641        // This is not ok - total voting power > BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER
642        authority.voting_power = BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER + 1;
643        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err();
644
645        // This is ok
646        authority.voting_power = 5000;
647        let mut authority_2 = authority.clone();
648        let (_, kp): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair();
649        let pubkey = kp.public().clone();
650        authority_2.pubkey = pubkey.clone();
651        let _ = BridgeCommittee::new(vec![authority.clone(), authority_2.clone()]).unwrap();
652
653        // This is not ok - duplicate pub key
654        authority_2.pubkey = authority.pubkey.clone();
655        let _ = BridgeCommittee::new(vec![authority.clone(), authority.clone()]).unwrap_err();
656        Ok(())
657    }
658
659    #[test]
660    fn test_bridge_committee_total_blocklisted_stake() -> anyhow::Result<()> {
661        let (mut authority1, _, _) = get_test_authority_and_key(10000, 9999);
662        assert_eq!(
663            BridgeCommittee::new(vec![authority1.clone()])
664                .unwrap()
665                .total_blocklisted_stake(),
666            0
667        );
668        authority1.voting_power = 6000;
669
670        let (mut authority2, _, _) = get_test_authority_and_key(4000, 9999);
671        authority2.is_blocklisted = true;
672        assert_eq!(
673            BridgeCommittee::new(vec![authority1.clone(), authority2.clone()])
674                .unwrap()
675                .total_blocklisted_stake(),
676            4000
677        );
678
679        authority1.voting_power = 7000;
680        authority2.voting_power = 2000;
681        let (mut authority3, _, _) = get_test_authority_and_key(1000, 9999);
682        authority3.is_blocklisted = true;
683        assert_eq!(
684            BridgeCommittee::new(vec![authority1, authority2, authority3])
685                .unwrap()
686                .total_blocklisted_stake(),
687            3000
688        );
689
690        Ok(())
691    }
692
693    // Regression test to avoid accidentally change to approval threshold
694    #[test]
695    fn test_bridge_action_approval_threshold_regression_test() -> anyhow::Result<()> {
696        let action = get_test_sui_to_eth_bridge_action(None, None, None, None, None, None, None);
697        assert_eq!(action.approval_threshold(), 3334);
698
699        let action = get_test_eth_to_sui_bridge_action(None, None, None, None);
700        assert_eq!(action.approval_threshold(), 3334);
701
702        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
703            nonce: 94,
704            chain_id: BridgeChainId::EthSepolia,
705            blocklist_type: BlocklistType::Unblocklist,
706            members_to_update: vec![],
707        });
708        assert_eq!(action.approval_threshold(), 5001);
709
710        let action = BridgeAction::EmergencyAction(EmergencyAction {
711            nonce: 56,
712            chain_id: BridgeChainId::EthSepolia,
713            action_type: EmergencyActionType::Pause,
714        });
715        assert_eq!(action.approval_threshold(), 450);
716
717        let action = BridgeAction::EmergencyAction(EmergencyAction {
718            nonce: 56,
719            chain_id: BridgeChainId::EthSepolia,
720            action_type: EmergencyActionType::Unpause,
721        });
722        assert_eq!(action.approval_threshold(), 5001);
723
724        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
725            nonce: 15,
726            chain_id: BridgeChainId::SuiCustom,
727            sending_chain_id: BridgeChainId::EthCustom,
728            new_usd_limit: 1_000_000 * USD_MULTIPLIER,
729        });
730        assert_eq!(action.approval_threshold(), 5001);
731
732        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
733            nonce: 266,
734            chain_id: BridgeChainId::SuiCustom,
735            token_id: TOKEN_ID_BTC,
736            new_usd_price: 100_000 * USD_MULTIPLIER,
737        });
738        assert_eq!(action.approval_threshold(), 5001);
739
740        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
741            nonce: 123,
742            chain_id: BridgeChainId::EthCustom,
743            proxy_address: EthAddress::repeat_byte(6),
744            new_impl_address: EthAddress::repeat_byte(9),
745            call_data: vec![],
746        });
747        assert_eq!(action.approval_threshold(), 5001);
748        Ok(())
749    }
750
751    #[test]
752    fn test_bridge_committee_filter_blocklisted_authorities() -> anyhow::Result<()> {
753        // Note: today BridgeCommittee does not shuffle authorities
754        let (authority1, _, _) = get_test_authority_and_key(5000, 9999);
755        let (mut authority2, _, _) = get_test_authority_and_key(3000, 9999);
756        authority2.is_blocklisted = true;
757        let (authority3, _, _) = get_test_authority_and_key(2000, 9999);
758        let committee = BridgeCommittee::new(vec![
759            authority1.clone(),
760            authority2.clone(),
761            authority3.clone(),
762        ])
763        .unwrap();
764
765        // exclude authority2
766        let result = committee
767            .shuffle_by_stake(None, None)
768            .into_iter()
769            .collect::<HashSet<_>>();
770        assert_eq!(
771            HashSet::from_iter(vec![authority1.pubkey_bytes(), authority3.pubkey_bytes()]),
772            result
773        );
774
775        // exclude authority2 and authority3
776        let result = committee
777            .shuffle_by_stake(
778                None,
779                Some(
780                    &[authority1.pubkey_bytes(), authority2.pubkey_bytes()]
781                        .iter()
782                        .cloned()
783                        .collect(),
784                ),
785            )
786            .into_iter()
787            .collect::<HashSet<_>>();
788        assert_eq!(HashSet::from_iter(vec![authority1.pubkey_bytes()]), result);
789
790        Ok(())
791    }
792}