sui_types/
bridge.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::SUI_BRIDGE_OBJECT_ID;
5use crate::base_types::ObjectID;
6use crate::base_types::SequenceNumber;
7use crate::collection_types::LinkedTableNode;
8use crate::dynamic_field::{Field, get_dynamic_field_from_store};
9use crate::error::{SuiError, SuiErrorKind, SuiResult};
10use crate::object::Owner;
11use crate::storage::ObjectStore;
12use crate::sui_serde::BigInt;
13use crate::sui_serde::Readable;
14use crate::versioned::Versioned;
15use crate::{
16    base_types::SuiAddress,
17    collection_types::{Bag, LinkedTable, VecMap},
18    id::UID,
19};
20use enum_dispatch::enum_dispatch;
21use move_core_types::ident_str;
22use move_core_types::identifier::IdentStr;
23use num_enum::TryFromPrimitive;
24use schemars::JsonSchema;
25use serde::{Deserialize, Serialize};
26use serde_with::serde_as;
27use strum_macros::Display;
28
29pub type BridgeInnerDynamicField = Field<u64, BridgeInnerV1>;
30pub type BridgeRecordDyanmicField = Field<
31    MoveTypeBridgeMessageKey,
32    LinkedTableNode<MoveTypeBridgeMessageKey, MoveTypeBridgeRecord>,
33>;
34
35pub const BRIDGE_MODULE_NAME: &IdentStr = ident_str!("bridge");
36pub const BRIDGE_TREASURY_MODULE_NAME: &IdentStr = ident_str!("treasury");
37pub const BRIDGE_LIMITER_MODULE_NAME: &IdentStr = ident_str!("limiter");
38pub const BRIDGE_COMMITTEE_MODULE_NAME: &IdentStr = ident_str!("committee");
39pub const BRIDGE_MESSAGE_MODULE_NAME: &IdentStr = ident_str!("message");
40pub const BRIDGE_CREATE_FUNCTION_NAME: &IdentStr = ident_str!("create");
41pub const BRIDGE_INIT_COMMITTEE_FUNCTION_NAME: &IdentStr = ident_str!("init_bridge_committee");
42pub const BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME: &IdentStr =
43    ident_str!("register_foreign_token");
44pub const BRIDGE_CREATE_ADD_TOKEN_ON_SUI_MESSAGE_FUNCTION_NAME: &IdentStr =
45    ident_str!("create_add_tokens_on_sui_message");
46pub const BRIDGE_EXECUTE_SYSTEM_MESSAGE_FUNCTION_NAME: &IdentStr =
47    ident_str!("execute_system_message");
48
49pub const BRIDGE_SUPPORTED_ASSET: &[&str] = &["btc", "eth", "usdc", "usdt"];
50
51pub const BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER: u64 = 7500; // out of 10000 (75%)
52pub const BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER: u64 = 10000; // (100%)
53
54// Threshold for action to be approved by the committee (our of 10000)
55pub const APPROVAL_THRESHOLD_TOKEN_TRANSFER: u64 = 3334;
56pub const APPROVAL_THRESHOLD_EMERGENCY_PAUSE: u64 = 450;
57pub const APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE: u64 = 5001;
58pub const APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST: u64 = 5001;
59pub const APPROVAL_THRESHOLD_LIMIT_UPDATE: u64 = 5001;
60pub const APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE: u64 = 5001;
61pub const APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE: u64 = 5001;
62pub const APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI: u64 = 5001;
63pub const APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM: u64 = 5001;
64
65// const for initial token ids for convenience
66pub const TOKEN_ID_SUI: u8 = 0;
67pub const TOKEN_ID_BTC: u8 = 1;
68pub const TOKEN_ID_ETH: u8 = 2;
69pub const TOKEN_ID_USDC: u8 = 3;
70pub const TOKEN_ID_USDT: u8 = 4;
71
72#[derive(
73    Debug,
74    Serialize,
75    Deserialize,
76    PartialEq,
77    Eq,
78    Clone,
79    Copy,
80    TryFromPrimitive,
81    JsonSchema,
82    Hash,
83    Display,
84)]
85#[repr(u8)]
86pub enum BridgeChainId {
87    SuiMainnet = 0,
88    SuiTestnet = 1,
89    SuiCustom = 2,
90
91    EthMainnet = 10,
92    EthSepolia = 11,
93    EthCustom = 12,
94}
95
96impl BridgeChainId {
97    pub fn is_sui_chain(&self) -> bool {
98        matches!(
99            self,
100            BridgeChainId::SuiMainnet | BridgeChainId::SuiTestnet | BridgeChainId::SuiCustom
101        )
102    }
103}
104
105pub fn get_bridge_obj_initial_shared_version(
106    object_store: &dyn ObjectStore,
107) -> SuiResult<Option<SequenceNumber>> {
108    Ok(object_store
109        .get_object(&SUI_BRIDGE_OBJECT_ID)
110        .map(|obj| match obj.owner {
111            Owner::Shared {
112                initial_shared_version,
113            } => initial_shared_version,
114            _ => unreachable!("Bridge object must be shared"),
115        }))
116}
117
118/// Bridge provides an abstraction over multiple versions of the inner BridgeInner object.
119/// This should be the primary interface to the bridge object in Rust.
120/// We use enum dispatch to dispatch all methods defined in BridgeTrait to the actual
121/// implementation in the inner types.
122#[derive(Debug, Serialize, Deserialize, Clone)]
123#[enum_dispatch(BridgeTrait)]
124pub enum Bridge {
125    V1(BridgeInnerV1),
126}
127
128/// Rust version of the Move sui::bridge::Bridge type
129/// This repreents the object with 0x9 ID.
130/// In Rust, this type should be rarely used since it's just a thin
131/// wrapper used to access the inner object.
132/// Within this module, we use it to determine the current version of the bridge inner object type,
133/// so that we could deserialize the inner object correctly.
134#[derive(Debug, Serialize, Deserialize, Clone)]
135pub struct BridgeWrapper {
136    pub id: UID,
137    pub version: Versioned,
138}
139
140/// This is the standard API that all bridge inner object type should implement.
141#[enum_dispatch]
142pub trait BridgeTrait {
143    fn bridge_version(&self) -> u64;
144    fn message_version(&self) -> u8;
145    fn chain_id(&self) -> u8;
146    fn sequence_nums(&self) -> &VecMap<u8, u64>;
147    fn committee(&self) -> &MoveTypeBridgeCommittee;
148    fn treasury(&self) -> &MoveTypeBridgeTreasury;
149    fn bridge_records(&self) -> &LinkedTable<MoveTypeBridgeMessageKey>;
150    fn frozen(&self) -> bool;
151    fn try_into_bridge_summary(self) -> SuiResult<BridgeSummary>;
152}
153
154#[serde_as]
155#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
156#[serde(rename_all = "camelCase")]
157pub struct BridgeSummary {
158    #[schemars(with = "BigInt<u64>")]
159    #[serde_as(as = "Readable<BigInt<u64>, _>")]
160    pub bridge_version: u64,
161    // Message version
162    pub message_version: u8,
163    /// Self Chain ID
164    pub chain_id: u8,
165    /// Sequence numbers of all message types
166    #[schemars(with = "Vec<(u8, BigInt<u64>)>")]
167    #[serde_as(as = "Vec<(_, Readable<BigInt<u64>, _>)>")]
168    pub sequence_nums: Vec<(u8, u64)>,
169    pub committee: BridgeCommitteeSummary,
170    /// Summary of the treasury
171    pub treasury: BridgeTreasurySummary,
172    /// Object ID of bridge Records (dynamic field)
173    pub bridge_records_id: ObjectID,
174    /// Summary of the limiter
175    pub limiter: BridgeLimiterSummary,
176    /// Whether the bridge is currently frozen or not
177    pub is_frozen: bool,
178    // TODO: add treasury
179}
180
181impl Default for BridgeSummary {
182    fn default() -> Self {
183        BridgeSummary {
184            bridge_version: 1,
185            message_version: 1,
186            chain_id: 1,
187            sequence_nums: vec![],
188            committee: BridgeCommitteeSummary::default(),
189            treasury: BridgeTreasurySummary::default(),
190            bridge_records_id: ObjectID::random(),
191            limiter: BridgeLimiterSummary::default(),
192            is_frozen: false,
193        }
194    }
195}
196
197pub fn get_bridge_wrapper(object_store: &dyn ObjectStore) -> Result<BridgeWrapper, SuiError> {
198    let wrapper = object_store
199        .get_object(&SUI_BRIDGE_OBJECT_ID)
200        // Don't panic here on None because object_store is a generic store.
201        .ok_or_else(|| {
202            SuiErrorKind::SuiBridgeReadError("BridgeWrapper object not found".to_owned())
203        })?;
204    let move_object = wrapper.data.try_as_move().ok_or_else(|| {
205        SuiErrorKind::SuiBridgeReadError("BridgeWrapper object must be a Move object".to_owned())
206    })?;
207    let result = bcs::from_bytes::<BridgeWrapper>(move_object.contents())
208        .map_err(|err| SuiErrorKind::SuiBridgeReadError(err.to_string()))?;
209    Ok(result)
210}
211
212pub fn get_bridge(object_store: &dyn ObjectStore) -> Result<Bridge, SuiError> {
213    let wrapper = get_bridge_wrapper(object_store)?;
214    let id = wrapper.version.id.id.bytes;
215    let version = wrapper.version.version;
216    match version {
217        1 => {
218            let result: BridgeInnerV1 = get_dynamic_field_from_store(object_store, id, &version)
219                .map_err(|err| {
220                    SuiErrorKind::SuiBridgeReadError(format!(
221                        "Failed to load bridge inner object with ID {:?} and version {:?}: {:?}",
222                        id, version, err
223                    ))
224                })?;
225            Ok(Bridge::V1(result))
226        }
227        _ => Err(SuiErrorKind::SuiBridgeReadError(format!(
228            "Unsupported SuiBridge version: {}",
229            version
230        ))
231        .into()),
232    }
233}
234
235/// Rust version of the Move bridge::BridgeInner type.
236#[derive(Debug, Serialize, Deserialize, Clone)]
237pub struct BridgeInnerV1 {
238    pub bridge_version: u64,
239    pub message_version: u8,
240    pub chain_id: u8,
241    pub sequence_nums: VecMap<u8, u64>,
242    pub committee: MoveTypeBridgeCommittee,
243    pub treasury: MoveTypeBridgeTreasury,
244    pub bridge_records: LinkedTable<MoveTypeBridgeMessageKey>,
245    pub limiter: MoveTypeBridgeTransferLimiter,
246    pub frozen: bool,
247}
248
249impl BridgeTrait for BridgeInnerV1 {
250    fn bridge_version(&self) -> u64 {
251        self.bridge_version
252    }
253
254    fn message_version(&self) -> u8 {
255        self.message_version
256    }
257
258    fn chain_id(&self) -> u8 {
259        self.chain_id
260    }
261
262    fn sequence_nums(&self) -> &VecMap<u8, u64> {
263        &self.sequence_nums
264    }
265
266    fn committee(&self) -> &MoveTypeBridgeCommittee {
267        &self.committee
268    }
269
270    fn treasury(&self) -> &MoveTypeBridgeTreasury {
271        &self.treasury
272    }
273
274    fn bridge_records(&self) -> &LinkedTable<MoveTypeBridgeMessageKey> {
275        &self.bridge_records
276    }
277
278    fn frozen(&self) -> bool {
279        self.frozen
280    }
281
282    fn try_into_bridge_summary(self) -> SuiResult<BridgeSummary> {
283        let transfer_limit = self
284            .limiter
285            .transfer_limit
286            .contents
287            .into_iter()
288            .map(|e| {
289                let source = BridgeChainId::try_from(e.key.source).map_err(|_e| {
290                    SuiErrorKind::GenericBridgeError {
291                        error: format!("Unrecognized chain id: {}", e.key.source),
292                    }
293                })?;
294                let destination = BridgeChainId::try_from(e.key.destination).map_err(|_e| {
295                    SuiErrorKind::GenericBridgeError {
296                        error: format!("Unrecognized chain id: {}", e.key.destination),
297                    }
298                })?;
299                Ok((source, destination, e.value))
300            })
301            .collect::<SuiResult<Vec<_>>>()?;
302        let supported_tokens = self
303            .treasury
304            .supported_tokens
305            .contents
306            .into_iter()
307            .map(|e| (e.key, e.value))
308            .collect::<Vec<_>>();
309        let id_token_type_map = self
310            .treasury
311            .id_token_type_map
312            .contents
313            .into_iter()
314            .map(|e| (e.key, e.value))
315            .collect::<Vec<_>>();
316        let transfer_records = self
317            .limiter
318            .transfer_records
319            .contents
320            .into_iter()
321            .map(|e| {
322                let source = BridgeChainId::try_from(e.key.source).map_err(|_e| {
323                    SuiErrorKind::GenericBridgeError {
324                        error: format!("Unrecognized chain id: {}", e.key.source),
325                    }
326                })?;
327                let destination = BridgeChainId::try_from(e.key.destination).map_err(|_e| {
328                    SuiErrorKind::GenericBridgeError {
329                        error: format!("Unrecognized chain id: {}", e.key.destination),
330                    }
331                })?;
332                Ok((source, destination, e.value))
333            })
334            .collect::<SuiResult<Vec<_>>>()?;
335        let limiter = BridgeLimiterSummary {
336            transfer_limit,
337            transfer_records,
338        };
339        Ok(BridgeSummary {
340            bridge_version: self.bridge_version,
341            message_version: self.message_version,
342            chain_id: self.chain_id,
343            sequence_nums: self
344                .sequence_nums
345                .contents
346                .into_iter()
347                .map(|e| (e.key, e.value))
348                .collect(),
349            committee: BridgeCommitteeSummary {
350                members: self
351                    .committee
352                    .members
353                    .contents
354                    .into_iter()
355                    .map(|e| (e.key, e.value))
356                    .collect(),
357                member_registration: self
358                    .committee
359                    .member_registrations
360                    .contents
361                    .into_iter()
362                    .map(|e| (e.key, e.value))
363                    .collect(),
364                last_committee_update_epoch: self.committee.last_committee_update_epoch,
365            },
366            bridge_records_id: self.bridge_records.id,
367            limiter,
368            treasury: BridgeTreasurySummary {
369                supported_tokens,
370                id_token_type_map,
371            },
372            is_frozen: self.frozen,
373        })
374    }
375}
376
377/// Rust version of the Move treasury::BridgeTreasury type.
378#[derive(Debug, Serialize, Deserialize, Clone)]
379pub struct MoveTypeBridgeTreasury {
380    pub treasuries: Bag,
381    pub supported_tokens: VecMap<String, BridgeTokenMetadata>,
382    // Mapping token id to type name
383    pub id_token_type_map: VecMap<u8, String>,
384    // Bag for storing potential new token waiting to be approved
385    pub waiting_room: Bag,
386}
387
388#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default, PartialEq, Eq)]
389#[serde(rename_all = "camelCase")]
390pub struct BridgeTokenMetadata {
391    pub id: u8,
392    pub decimal_multiplier: u64,
393    pub notional_value: u64,
394    pub native_token: bool,
395}
396
397/// Rust version of the Move committee::BridgeCommittee type.
398#[derive(Debug, Serialize, Deserialize, Clone)]
399pub struct MoveTypeBridgeCommittee {
400    pub members: VecMap<Vec<u8>, MoveTypeCommitteeMember>,
401    pub member_registrations: VecMap<SuiAddress, MoveTypeCommitteeMemberRegistration>,
402    pub last_committee_update_epoch: u64,
403}
404
405/// Rust version of the Move committee::CommitteeMemberRegistration type.
406#[serde_as]
407#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default, PartialEq, Eq)]
408#[serde(rename_all = "camelCase")]
409pub struct MoveTypeCommitteeMemberRegistration {
410    pub sui_address: SuiAddress,
411    pub bridge_pubkey_bytes: Vec<u8>,
412    pub http_rest_url: Vec<u8>,
413}
414
415#[serde_as]
416#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
417#[serde(rename_all = "camelCase")]
418pub struct BridgeCommitteeSummary {
419    pub members: Vec<(Vec<u8>, MoveTypeCommitteeMember)>,
420    pub member_registration: Vec<(SuiAddress, MoveTypeCommitteeMemberRegistration)>,
421    pub last_committee_update_epoch: u64,
422}
423
424#[serde_as]
425#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
426#[serde(rename_all = "camelCase")]
427pub struct BridgeLimiterSummary {
428    pub transfer_limit: Vec<(BridgeChainId, BridgeChainId, u64)>,
429    pub transfer_records: Vec<(BridgeChainId, BridgeChainId, MoveTypeBridgeTransferRecord)>,
430}
431
432#[serde_as]
433#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
434#[serde(rename_all = "camelCase")]
435pub struct BridgeTreasurySummary {
436    pub supported_tokens: Vec<(String, BridgeTokenMetadata)>,
437    pub id_token_type_map: Vec<(u8, String)>,
438}
439
440/// Rust version of the Move committee::CommitteeMember type.
441#[serde_as]
442#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default, Eq, PartialEq)]
443#[serde(rename_all = "camelCase")]
444pub struct MoveTypeCommitteeMember {
445    pub sui_address: SuiAddress,
446    pub bridge_pubkey_bytes: Vec<u8>,
447    pub voting_power: u64,
448    pub http_rest_url: Vec<u8>,
449    pub blocklisted: bool,
450}
451
452/// Rust version of the Move message::BridgeMessageKey type.
453#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
454pub struct MoveTypeBridgeMessageKey {
455    pub source_chain: u8,
456    pub message_type: u8,
457    pub bridge_seq_num: u64,
458}
459
460/// Rust version of the Move limiter::TransferLimiter type.
461#[derive(Debug, Serialize, Deserialize, Clone)]
462pub struct MoveTypeBridgeTransferLimiter {
463    pub transfer_limit: VecMap<MoveTypeBridgeRoute, u64>,
464    pub transfer_records: VecMap<MoveTypeBridgeRoute, MoveTypeBridgeTransferRecord>,
465}
466
467/// Rust version of the Move chain_ids::BridgeRoute type.
468#[derive(Debug, Serialize, Deserialize, Clone)]
469pub struct MoveTypeBridgeRoute {
470    pub source: u8,
471    pub destination: u8,
472}
473
474/// Rust version of the Move limiter::TransferRecord type.
475#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
476pub struct MoveTypeBridgeTransferRecord {
477    hour_head: u64,
478    hour_tail: u64,
479    per_hour_amounts: Vec<u64>,
480    total_amount: u64,
481}
482
483/// Rust version of the Move message::BridgeMessage type.
484#[derive(Debug, Serialize, Deserialize)]
485pub struct MoveTypeBridgeMessage {
486    pub message_type: u8,
487    pub message_version: u8,
488    pub seq_num: u64,
489    pub source_chain: u8,
490    pub payload: Vec<u8>,
491}
492
493/// Rust version of the Move message::BridgeMessage type.
494#[derive(Debug, Serialize, Deserialize)]
495pub struct MoveTypeBridgeRecord {
496    pub message: MoveTypeBridgeMessage,
497    pub verified_signatures: Option<Vec<Vec<u8>>>,
498    pub claimed: bool,
499}
500
501pub fn is_bridge_committee_initiated(object_store: &dyn ObjectStore) -> SuiResult<bool> {
502    match get_bridge(object_store).map_err(|e| *e.0) {
503        Ok(bridge) => Ok(!bridge.committee().members.contents.is_empty()),
504        Err(SuiErrorKind::SuiBridgeReadError(..)) => Ok(false),
505        Err(other) => Err(other.into()),
506    }
507}
508
509/// Rust version of the Move message::TokenTransferPayload type.
510#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
511pub struct MoveTypeTokenTransferPayload {
512    pub sender_address: Vec<u8>,
513    pub target_chain: u8,
514    pub target_address: Vec<u8>,
515    pub token_type: u8,
516    pub amount: u64,
517}
518
519/// Rust version of the Move message::ParsedTokenTransferMessage type.
520#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
521pub struct MoveTypeParsedTokenTransferMessage {
522    pub message_version: u8,
523    pub seq_num: u64,
524    pub source_chain: u8,
525    pub payload: Vec<u8>,
526    pub parsed_payload: MoveTypeTokenTransferPayload,
527}