iota_bridge/
types.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    fmt::Debug,
8};
9
10use enum_dispatch::enum_dispatch;
11pub use ethers::types::H256 as EthTransactionHash;
12use ethers::types::{Address as EthAddress, H256, Log};
13use fastcrypto::hash::{HashFunction, Keccak256};
14use iota_types::{
15    TypeTag,
16    bridge::{
17        APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM, APPROVAL_THRESHOLD_ADD_TOKENS_ON_IOTA,
18        APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE, APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST,
19        APPROVAL_THRESHOLD_EMERGENCY_PAUSE, APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE,
20        APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE, APPROVAL_THRESHOLD_LIMIT_UPDATE,
21        APPROVAL_THRESHOLD_TOKEN_TRANSFER, BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER,
22        BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER, BridgeChainId, MoveTypeParsedTokenTransferMessage,
23        MoveTypeTokenTransferPayload,
24    },
25    committee::{CommitteeTrait, StakeUnit},
26    digests::{Digest, TransactionDigest},
27    message_envelope::{Envelope, Message, VerifiedEnvelope},
28};
29use num_enum::TryFromPrimitive;
30use rand::{Rng, seq::SliceRandom};
31use serde::{Deserialize, Serialize};
32use shared_crypto::intent::IntentScope;
33
34use crate::{
35    abi::EthToIotaTokenBridgeV1,
36    crypto::{
37        BridgeAuthorityPublicKey, BridgeAuthorityPublicKeyBytes,
38        BridgeAuthorityRecoverableSignature, BridgeAuthoritySignInfo,
39    },
40    encoding::BridgeMessageEncoding,
41    error::{BridgeError, BridgeResult},
42    events::EmittedIotaToEthTokenBridgeV1,
43};
44
45pub const BRIDGE_AUTHORITY_TOTAL_VOTING_POWER: u64 = 10000;
46
47pub const USD_MULTIPLIER: u64 = 10000; // decimal places = 4
48
49pub type IsBridgePaused = bool;
50pub const BRIDGE_PAUSED: bool = true;
51pub const BRIDGE_UNPAUSED: bool = false;
52
53#[derive(Debug, Eq, PartialEq, Clone)]
54pub struct BridgeAuthority {
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
125impl CommitteeTrait<BridgeAuthorityPublicKeyBytes> for BridgeCommittee {
126    // Note: blocklisted members are always excluded.
127    fn shuffle_by_stake_with_rng(
128        &self,
129        // `preferences` is used as a *flag* here to influence the order of validators to be
130        // requested.
131        //  * if `Some(_)`, then we will request validators in the order of the voting power
132        //  * if `None`, we still refer to voting power, but they are shuffled by randomness.
133        //  to save gas cost.
134        preferences: Option<&BTreeSet<BridgeAuthorityPublicKeyBytes>>,
135        // only attempt from these authorities.
136        restrict_to: Option<&BTreeSet<BridgeAuthorityPublicKeyBytes>>,
137        rng: &mut impl Rng,
138    ) -> Vec<BridgeAuthorityPublicKeyBytes> {
139        let mut candidates = self
140            .members
141            .iter()
142            .filter_map(|(name, a)| {
143                // Remove blocklisted members
144                if a.is_blocklisted {
145                    return None;
146                }
147                // exclude non-allowlisted members
148                if let Some(restrict_to) = restrict_to {
149                    match restrict_to.contains(name) {
150                        true => Some((name.clone(), a.voting_power)),
151                        false => None,
152                    }
153                } else {
154                    Some((name.clone(), a.voting_power))
155                }
156            })
157            .collect::<Vec<_>>();
158        if preferences.is_some() {
159            candidates.sort_by(|(_, a), (_, b)| b.cmp(a));
160            candidates.iter().map(|(name, _)| name.clone()).collect()
161        } else {
162            candidates
163                .choose_multiple_weighted(rng, candidates.len(), |(_, weight)| *weight as f64)
164                // Unwrap safe: it panics when the third parameter is larger than the size of the
165                // slice
166                .unwrap()
167                .map(|(name, _)| name)
168                .cloned()
169                .collect()
170        }
171    }
172
173    fn weight(&self, author: &BridgeAuthorityPublicKeyBytes) -> StakeUnit {
174        self.members
175            .get(author)
176            .map(|a| a.voting_power)
177            .unwrap_or(0)
178    }
179}
180
181#[derive(Serialize, Copy, Clone, PartialEq, Eq, TryFromPrimitive, Hash)]
182#[repr(u8)]
183pub enum BridgeActionType {
184    TokenTransfer = 0,
185    UpdateCommitteeBlocklist = 1,
186    EmergencyButton = 2,
187    LimitUpdate = 3,
188    AssetPriceUpdate = 4,
189    EvmContractUpgrade = 5,
190    AddTokensOnIota = 6,
191    AddTokensOnEvm = 7,
192}
193
194#[derive(Clone, PartialEq, Eq)]
195pub struct BridgeActionKey {
196    pub action_type: BridgeActionType,
197    pub chain_id: BridgeChainId,
198    pub seq_num: u64,
199}
200
201impl Debug for BridgeActionKey {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(
204            f,
205            "BridgeActionKey({},{},{})",
206            self.action_type as u8, self.chain_id as u8, self.seq_num
207        )
208    }
209}
210
211#[derive(Debug, PartialEq, Eq, Clone, TryFromPrimitive)]
212#[repr(u8)]
213pub enum BridgeActionStatus {
214    Pending = 0,
215    Approved = 1,
216    Claimed = 2,
217    NotFound = 3,
218}
219
220#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
221pub struct IotaToEthBridgeAction {
222    // Digest of the transaction where the event was emitted
223    pub iota_tx_digest: TransactionDigest,
224    // The index of the event in the transaction
225    pub iota_tx_event_index: u16,
226    pub iota_bridge_event: EmittedIotaToEthTokenBridgeV1,
227}
228
229#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
230pub struct EthToIotaBridgeAction {
231    // Digest of the transaction where the event was emitted
232    pub eth_tx_hash: EthTransactionHash,
233    // The index of the event in the transaction
234    pub eth_event_index: u16,
235    pub eth_bridge_event: EthToIotaTokenBridgeV1,
236}
237
238#[derive(
239    Debug,
240    Serialize,
241    Deserialize,
242    PartialEq,
243    Eq,
244    Clone,
245    Copy,
246    TryFromPrimitive,
247    Hash,
248    clap::ValueEnum,
249)]
250#[repr(u8)]
251pub enum BlocklistType {
252    Blocklist = 0,
253    Unblocklist = 1,
254}
255
256#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
257pub struct BlocklistCommitteeAction {
258    pub nonce: u64,
259    pub chain_id: BridgeChainId,
260    pub blocklist_type: BlocklistType,
261    pub members_to_update: Vec<BridgeAuthorityPublicKeyBytes>,
262}
263
264#[derive(
265    Debug,
266    Serialize,
267    Deserialize,
268    PartialEq,
269    Eq,
270    Clone,
271    Copy,
272    TryFromPrimitive,
273    Hash,
274    clap::ValueEnum,
275)]
276#[repr(u8)]
277pub enum EmergencyActionType {
278    Pause = 0,
279    Unpause = 1,
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
283pub struct EmergencyAction {
284    pub nonce: u64,
285    pub chain_id: BridgeChainId,
286    pub action_type: EmergencyActionType,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
290pub struct LimitUpdateAction {
291    pub nonce: u64,
292    // The chain id that will receive this signed action. It's also the destination chain id
293    // for the limit update. For example, if chain_id is EthMainnet and sending_chain_id is
294    // IotaMainnet, it means we want to update the limit for the IotaMainnet to EthMainnet
295    // route.
296    pub chain_id: BridgeChainId,
297    // The sending chain id for the limit update.
298    pub sending_chain_id: BridgeChainId,
299    // 4 decimal places, namely 1 USD = 10000
300    pub new_usd_limit: u64,
301}
302
303#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
304pub struct AssetPriceUpdateAction {
305    pub nonce: u64,
306    pub chain_id: BridgeChainId,
307    pub token_id: u8,
308    pub new_usd_price: u64,
309}
310
311#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
312pub struct EvmContractUpgradeAction {
313    pub nonce: u64,
314    pub chain_id: BridgeChainId,
315    pub proxy_address: EthAddress,
316    pub new_impl_address: EthAddress,
317    pub call_data: Vec<u8>,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
321pub struct AddTokensOnIotaAction {
322    pub nonce: u64,
323    pub chain_id: BridgeChainId,
324    pub native: bool,
325    pub token_ids: Vec<u8>,
326    pub token_type_names: Vec<TypeTag>,
327    pub token_prices: Vec<u64>,
328}
329
330#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
331pub struct AddTokensOnEvmAction {
332    pub nonce: u64,
333    pub chain_id: BridgeChainId,
334    pub native: bool,
335    pub token_ids: Vec<u8>,
336    pub token_addresses: Vec<EthAddress>,
337    pub token_iota_decimals: Vec<u8>,
338    pub token_prices: Vec<u64>,
339}
340
341/// The type of actions Bridge Committee verify and sign off to execution.
342/// Its relationship with BridgeEvent is similar to the relationship between
343#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
344#[enum_dispatch(BridgeMessageEncoding)]
345pub enum BridgeAction {
346    /// IOTA to Eth bridge action
347    IotaToEthBridgeAction(IotaToEthBridgeAction),
348    /// Eth to iota bridge action
349    EthToIotaBridgeAction(EthToIotaBridgeAction),
350    BlocklistCommitteeAction(BlocklistCommitteeAction),
351    EmergencyAction(EmergencyAction),
352    LimitUpdateAction(LimitUpdateAction),
353    AssetPriceUpdateAction(AssetPriceUpdateAction),
354    EvmContractUpgradeAction(EvmContractUpgradeAction),
355    AddTokensOnIotaAction(AddTokensOnIotaAction),
356    AddTokensOnEvmAction(AddTokensOnEvmAction),
357}
358
359impl BridgeAction {
360    // Digest of BridgeAction (with Keccak256 hasher)
361    pub fn digest(&self) -> BridgeActionDigest {
362        let mut hasher = Keccak256::default();
363        hasher.update(self.to_bytes());
364        BridgeActionDigest::new(hasher.finalize().into())
365    }
366
367    pub fn key(&self) -> BridgeActionKey {
368        BridgeActionKey {
369            action_type: self.action_type(),
370            chain_id: self.chain_id(),
371            seq_num: self.seq_number(),
372        }
373    }
374
375    pub fn chain_id(&self) -> BridgeChainId {
376        match self {
377            BridgeAction::IotaToEthBridgeAction(a) => a.iota_bridge_event.iota_chain_id,
378            BridgeAction::EthToIotaBridgeAction(a) => a.eth_bridge_event.eth_chain_id,
379            BridgeAction::BlocklistCommitteeAction(a) => a.chain_id,
380            BridgeAction::EmergencyAction(a) => a.chain_id,
381            BridgeAction::LimitUpdateAction(a) => a.chain_id,
382            BridgeAction::AssetPriceUpdateAction(a) => a.chain_id,
383            BridgeAction::EvmContractUpgradeAction(a) => a.chain_id,
384            BridgeAction::AddTokensOnIotaAction(a) => a.chain_id,
385            BridgeAction::AddTokensOnEvmAction(a) => a.chain_id,
386        }
387    }
388
389    pub fn is_governace_action(&self) -> bool {
390        match self.action_type() {
391            BridgeActionType::TokenTransfer => false,
392            BridgeActionType::UpdateCommitteeBlocklist => true,
393            BridgeActionType::EmergencyButton => true,
394            BridgeActionType::LimitUpdate => true,
395            BridgeActionType::AssetPriceUpdate => true,
396            BridgeActionType::EvmContractUpgrade => true,
397            BridgeActionType::AddTokensOnIota => true,
398            BridgeActionType::AddTokensOnEvm => true,
399        }
400    }
401
402    // Also called `message_type`
403    pub fn action_type(&self) -> BridgeActionType {
404        match self {
405            BridgeAction::IotaToEthBridgeAction(_) => BridgeActionType::TokenTransfer,
406            BridgeAction::EthToIotaBridgeAction(_) => BridgeActionType::TokenTransfer,
407            BridgeAction::BlocklistCommitteeAction(_) => BridgeActionType::UpdateCommitteeBlocklist,
408            BridgeAction::EmergencyAction(_) => BridgeActionType::EmergencyButton,
409            BridgeAction::LimitUpdateAction(_) => BridgeActionType::LimitUpdate,
410            BridgeAction::AssetPriceUpdateAction(_) => BridgeActionType::AssetPriceUpdate,
411            BridgeAction::EvmContractUpgradeAction(_) => BridgeActionType::EvmContractUpgrade,
412            BridgeAction::AddTokensOnIotaAction(_) => BridgeActionType::AddTokensOnIota,
413            BridgeAction::AddTokensOnEvmAction(_) => BridgeActionType::AddTokensOnEvm,
414        }
415    }
416
417    // Also called `nonce`
418    pub fn seq_number(&self) -> u64 {
419        match self {
420            BridgeAction::IotaToEthBridgeAction(a) => a.iota_bridge_event.nonce,
421            BridgeAction::EthToIotaBridgeAction(a) => a.eth_bridge_event.nonce,
422            BridgeAction::BlocklistCommitteeAction(a) => a.nonce,
423            BridgeAction::EmergencyAction(a) => a.nonce,
424            BridgeAction::LimitUpdateAction(a) => a.nonce,
425            BridgeAction::AssetPriceUpdateAction(a) => a.nonce,
426            BridgeAction::EvmContractUpgradeAction(a) => a.nonce,
427            BridgeAction::AddTokensOnIotaAction(a) => a.nonce,
428            BridgeAction::AddTokensOnEvmAction(a) => a.nonce,
429        }
430    }
431
432    pub fn approval_threshold(&self) -> u64 {
433        match self {
434            BridgeAction::IotaToEthBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
435            BridgeAction::EthToIotaBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER,
436            BridgeAction::BlocklistCommitteeAction(_) => APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST,
437            BridgeAction::EmergencyAction(a) => match a.action_type {
438                EmergencyActionType::Pause => APPROVAL_THRESHOLD_EMERGENCY_PAUSE,
439                EmergencyActionType::Unpause => APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE,
440            },
441            BridgeAction::LimitUpdateAction(_) => APPROVAL_THRESHOLD_LIMIT_UPDATE,
442            BridgeAction::AssetPriceUpdateAction(_) => APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE,
443            BridgeAction::EvmContractUpgradeAction(_) => APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE,
444            BridgeAction::AddTokensOnIotaAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_IOTA,
445            BridgeAction::AddTokensOnEvmAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM,
446        }
447    }
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
451pub struct BridgeActionDigest(Digest);
452
453impl BridgeActionDigest {
454    pub const fn new(digest: [u8; 32]) -> Self {
455        Self(Digest::new(digest))
456    }
457}
458
459#[derive(Debug, Clone)]
460pub struct BridgeCommitteeValiditySignInfo {
461    pub signatures: BTreeMap<BridgeAuthorityPublicKeyBytes, BridgeAuthorityRecoverableSignature>,
462}
463
464pub type SignedBridgeAction = Envelope<BridgeAction, BridgeAuthoritySignInfo>;
465pub type VerifiedSignedBridgeAction = VerifiedEnvelope<BridgeAction, BridgeAuthoritySignInfo>;
466pub type CertifiedBridgeAction = Envelope<BridgeAction, BridgeCommitteeValiditySignInfo>;
467pub type VerifiedCertifiedBridgeAction =
468    VerifiedEnvelope<BridgeAction, BridgeCommitteeValiditySignInfo>;
469
470#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
471pub struct BridgeEventDigest(Digest);
472
473impl BridgeEventDigest {
474    pub const fn new(digest: [u8; 32]) -> Self {
475        Self(Digest::new(digest))
476    }
477}
478
479impl Message for BridgeAction {
480    type DigestType = BridgeEventDigest;
481
482    // this is not encoded in message today
483    const SCOPE: IntentScope = IntentScope::BridgeEventUnused;
484
485    // this is not used today
486    fn digest(&self) -> Self::DigestType {
487        unreachable!("BridgeEventDigest is not used today")
488    }
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub struct EthLog {
493    pub block_number: u64,
494    pub tx_hash: H256,
495    pub log_index_in_tx: u16,
496    pub log: Log,
497}
498
499/// The version of EthLog that does not have
500/// `log_index_in_tx`.
501#[derive(Debug, Clone, PartialEq, Eq)]
502pub struct RawEthLog {
503    pub block_number: u64,
504    pub tx_hash: H256,
505    pub log: Log,
506}
507
508pub trait EthEvent {
509    fn block_number(&self) -> u64;
510    fn tx_hash(&self) -> H256;
511    fn log(&self) -> &Log;
512}
513
514impl EthEvent for EthLog {
515    fn block_number(&self) -> u64 {
516        self.block_number
517    }
518    fn tx_hash(&self) -> H256 {
519        self.tx_hash
520    }
521    fn log(&self) -> &Log {
522        &self.log
523    }
524}
525
526impl EthEvent for RawEthLog {
527    fn block_number(&self) -> u64 {
528        self.block_number
529    }
530    fn tx_hash(&self) -> H256 {
531        self.tx_hash
532    }
533    fn log(&self) -> &Log {
534        &self.log
535    }
536}
537
538/// Check if the bridge route is valid
539/// Only mainnet can bridge to mainnet, other than that we do not care.
540pub fn is_route_valid(one: BridgeChainId, other: BridgeChainId) -> bool {
541    if one.is_iota_chain() && other.is_iota_chain() {
542        return false;
543    }
544    if !one.is_iota_chain() && !other.is_iota_chain() {
545        return false;
546    }
547    if one == BridgeChainId::EthMainnet {
548        return other == BridgeChainId::IotaMainnet;
549    }
550    if one == BridgeChainId::IotaMainnet {
551        return other == BridgeChainId::EthMainnet;
552    }
553    if other == BridgeChainId::EthMainnet {
554        return one == BridgeChainId::IotaMainnet;
555    }
556    if other == BridgeChainId::IotaMainnet {
557        return one == BridgeChainId::EthMainnet;
558    }
559    true
560}
561
562// Sanitized version of MoveTypeParsedTokenTransferMessage
563#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
564pub struct ParsedTokenTransferMessage {
565    pub message_version: u8,
566    pub seq_num: u64,
567    pub source_chain: BridgeChainId,
568    pub payload: Vec<u8>,
569    pub parsed_payload: MoveTypeTokenTransferPayload,
570}
571
572impl TryFrom<MoveTypeParsedTokenTransferMessage> for ParsedTokenTransferMessage {
573    type Error = BridgeError;
574
575    fn try_from(message: MoveTypeParsedTokenTransferMessage) -> BridgeResult<Self> {
576        let source_chain = BridgeChainId::try_from(message.source_chain).map_err(|_e| {
577            BridgeError::Generic(format!(
578                "Failed to convert MoveTypeParsedTokenTransferMessage to ParsedTokenTransferMessage. Failed to convert source chain {} to BridgeChainId",
579                message.source_chain,
580            ))
581        })?;
582        Ok(Self {
583            message_version: message.message_version,
584            seq_num: message.seq_num,
585            source_chain,
586            payload: message.payload,
587            parsed_payload: message.parsed_payload,
588        })
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use std::collections::HashSet;
595
596    use ethers::types::Address as EthAddress;
597    use fastcrypto::traits::KeyPair;
598    use iota_types::{bridge::TOKEN_ID_BTC, crypto::get_key_pair};
599
600    use super::*;
601    use crate::test_utils::{
602        get_test_authority_and_key, get_test_eth_to_iota_bridge_action,
603        get_test_iota_to_eth_bridge_action,
604    };
605
606    #[test]
607    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
608    fn test_bridge_committee_construction() -> anyhow::Result<()> {
609        let (mut authority, _, _) = get_test_authority_and_key(8000, 9999);
610        // This is ok
611        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap();
612
613        // This is not ok - total voting power < BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER
614        authority.voting_power = BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER - 1;
615        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err();
616
617        // This is not ok - total voting power > BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER
618        authority.voting_power = BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER + 1;
619        let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err();
620
621        // This is ok
622        authority.voting_power = 5000;
623        let mut authority_2 = authority.clone();
624        let (_, kp): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair();
625        let pubkey = kp.public().clone();
626        authority_2.pubkey = pubkey.clone();
627        let _ = BridgeCommittee::new(vec![authority.clone(), authority_2.clone()]).unwrap();
628
629        // This is not ok - duplicate pub key
630        authority_2.pubkey = authority.pubkey.clone();
631        let _ = BridgeCommittee::new(vec![authority.clone(), authority.clone()]).unwrap_err();
632        Ok(())
633    }
634
635    #[test]
636    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
637    fn test_bridge_committee_total_blocklisted_stake() -> anyhow::Result<()> {
638        let (mut authority1, _, _) = get_test_authority_and_key(10000, 9999);
639        assert_eq!(
640            BridgeCommittee::new(vec![authority1.clone()])
641                .unwrap()
642                .total_blocklisted_stake(),
643            0
644        );
645        authority1.voting_power = 6000;
646
647        let (mut authority2, _, _) = get_test_authority_and_key(4000, 9999);
648        authority2.is_blocklisted = true;
649        assert_eq!(
650            BridgeCommittee::new(vec![authority1.clone(), authority2.clone()])
651                .unwrap()
652                .total_blocklisted_stake(),
653            4000
654        );
655
656        authority1.voting_power = 7000;
657        authority2.voting_power = 2000;
658        let (mut authority3, _, _) = get_test_authority_and_key(1000, 9999);
659        authority3.is_blocklisted = true;
660        assert_eq!(
661            BridgeCommittee::new(vec![authority1, authority2, authority3])
662                .unwrap()
663                .total_blocklisted_stake(),
664            3000
665        );
666
667        Ok(())
668    }
669
670    // Regression test to avoid accidentally change to approval threshold
671    #[test]
672    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
673    fn test_bridge_action_approval_threshold_regression_test() -> anyhow::Result<()> {
674        let action = get_test_iota_to_eth_bridge_action(None, None, None, None, None, None, None);
675        assert_eq!(action.approval_threshold(), 3334);
676
677        let action = get_test_eth_to_iota_bridge_action(None, None, None, None);
678        assert_eq!(action.approval_threshold(), 3334);
679
680        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
681            nonce: 94,
682            chain_id: BridgeChainId::EthSepolia,
683            blocklist_type: BlocklistType::Unblocklist,
684            members_to_update: vec![],
685        });
686        assert_eq!(action.approval_threshold(), 5001);
687
688        let action = BridgeAction::EmergencyAction(EmergencyAction {
689            nonce: 56,
690            chain_id: BridgeChainId::EthSepolia,
691            action_type: EmergencyActionType::Pause,
692        });
693        assert_eq!(action.approval_threshold(), 450);
694
695        let action = BridgeAction::EmergencyAction(EmergencyAction {
696            nonce: 56,
697            chain_id: BridgeChainId::EthSepolia,
698            action_type: EmergencyActionType::Unpause,
699        });
700        assert_eq!(action.approval_threshold(), 5001);
701
702        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
703            nonce: 15,
704            chain_id: BridgeChainId::IotaCustom,
705            sending_chain_id: BridgeChainId::EthCustom,
706            new_usd_limit: 1_000_000 * USD_MULTIPLIER,
707        });
708        assert_eq!(action.approval_threshold(), 5001);
709
710        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
711            nonce: 266,
712            chain_id: BridgeChainId::IotaCustom,
713            token_id: TOKEN_ID_BTC,
714            new_usd_price: 100_000 * USD_MULTIPLIER,
715        });
716        assert_eq!(action.approval_threshold(), 5001);
717
718        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
719            nonce: 123,
720            chain_id: BridgeChainId::EthCustom,
721            proxy_address: EthAddress::repeat_byte(6),
722            new_impl_address: EthAddress::repeat_byte(9),
723            call_data: vec![],
724        });
725        assert_eq!(action.approval_threshold(), 5001);
726        Ok(())
727    }
728
729    #[test]
730    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
731    fn test_bridge_committee_filter_blocklisted_authorities() -> anyhow::Result<()> {
732        // Note: today BridgeCommittee does not shuffle authorities
733        let (authority1, _, _) = get_test_authority_and_key(5000, 9999);
734        let (mut authority2, _, _) = get_test_authority_and_key(3000, 9999);
735        authority2.is_blocklisted = true;
736        let (authority3, _, _) = get_test_authority_and_key(2000, 9999);
737        let committee = BridgeCommittee::new(vec![
738            authority1.clone(),
739            authority2.clone(),
740            authority3.clone(),
741        ])
742        .unwrap();
743
744        // exclude authority2
745        let result = committee
746            .shuffle_by_stake(None, None)
747            .into_iter()
748            .collect::<HashSet<_>>();
749        assert_eq!(
750            HashSet::from_iter(vec![authority1.pubkey_bytes(), authority3.pubkey_bytes()]),
751            result
752        );
753
754        // exclude authority2 and authority3
755        let result = committee
756            .shuffle_by_stake(
757                None,
758                Some(
759                    &[authority1.pubkey_bytes(), authority2.pubkey_bytes()]
760                        .iter()
761                        .cloned()
762                        .collect(),
763                ),
764            )
765            .into_iter()
766            .collect::<HashSet<_>>();
767        assert_eq!(HashSet::from_iter(vec![authority1.pubkey_bytes()]), result);
768
769        Ok(())
770    }
771}