iota_types/
bridge.rs

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