iota_bridge/
abi.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use ethers::{
6    abi::RawLog,
7    contract::{EthLogDecode, abigen},
8    types::{Address as EthAddress, Log},
9};
10use iota_types::{base_types::IotaAddress, bridge::BridgeChainId};
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    encoding::{
15        ADD_TOKENS_ON_EVM_MESSAGE_VERSION, ASSET_PRICE_UPDATE_MESSAGE_VERSION,
16        BridgeMessageEncoding, COMMITTEE_BLOCKLIST_MESSAGE_VERSION,
17        EMERGENCY_BUTTON_MESSAGE_VERSION, EVM_CONTRACT_UPGRADE_MESSAGE_VERSION,
18        LIMIT_UPDATE_MESSAGE_VERSION, TOKEN_TRANSFER_MESSAGE_VERSION,
19    },
20    error::{BridgeError, BridgeResult},
21    types::{
22        AddTokensOnEvmAction, AssetPriceUpdateAction, BlocklistCommitteeAction, BridgeAction,
23        BridgeActionType, EmergencyAction, EthLog, EthToIotaBridgeAction, EvmContractUpgradeAction,
24        IotaToEthBridgeAction, LimitUpdateAction, ParsedTokenTransferMessage,
25    },
26};
27
28macro_rules! gen_eth_events {
29    ($($contract:ident, $contract_event:ident, $abi_path:literal),* $(,)?) => {
30        $(
31            abigen!(
32                $contract,
33                $abi_path,
34                event_derives(serde::Deserialize, serde::Serialize)
35            );
36        )*
37
38        #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39        pub enum EthBridgeEvent {
40            $(
41                $contract_event($contract_event),
42            )*
43        }
44
45        impl EthBridgeEvent {
46            pub fn try_from_eth_log(log: &EthLog) -> Option<EthBridgeEvent> {
47                Self::try_from_log(&log.log)
48            }
49
50            pub fn try_from_log(log: &Log) -> Option<EthBridgeEvent> {
51                let raw_log = RawLog {
52                    topics: log.topics.clone(),
53                    data: log.data.to_vec(),
54                };
55
56                $(
57                    if let Ok(decoded) = $contract_event::decode_log(&raw_log) {
58                        return Some(EthBridgeEvent::$contract_event(decoded));
59                    }
60                )*
61
62                None
63            }
64        }
65    };
66
67    // For contracts that don't have Events
68    ($($contract:ident, $abi_path:literal),* $(,)?) => {
69        $(
70            abigen!(
71                $contract,
72                $abi_path,
73                event_derives(serde::Deserialize, serde::Serialize)
74            );
75        )*
76    };
77}
78
79#[rustfmt::skip]
80gen_eth_events!(
81    EthIotaBridge, EthIotaBridgeEvents, "abi/iota_bridge.json",
82    EthBridgeCommittee, EthBridgeCommitteeEvents, "abi/bridge_committee.json",
83    EthBridgeLimiter, EthBridgeLimiterEvents, "abi/bridge_limiter.json",
84    EthBridgeConfig, EthBridgeConfigEvents, "abi/bridge_config.json",
85    EthCommitteeUpgradeableContract, EthCommitteeUpgradeableContractEvents, "abi/bridge_committee_upgradeable.json"
86);
87
88gen_eth_events!(EthBridgeVault, "abi/bridge_vault.json");
89
90abigen!(
91    EthERC20,
92    "abi/erc20.json",
93    event_derives(serde::Deserialize, serde::Serialize)
94);
95
96impl EthBridgeEvent {
97    pub fn try_into_bridge_action(
98        self,
99        eth_tx_hash: ethers::types::H256,
100        eth_event_index: u16,
101    ) -> BridgeResult<Option<BridgeAction>> {
102        Ok(match self {
103            EthBridgeEvent::EthIotaBridgeEvents(event) => {
104                match event {
105                    EthIotaBridgeEvents::TokensDepositedFilter(event) => {
106                        let bridge_event = match EthToIotaTokenBridgeV1::try_from(&event) {
107                            Ok(bridge_event) => {
108                                if bridge_event.iota_adjusted_amount == 0 {
109                                    return Err(BridgeError::ZeroValueBridgeTransfer(format!(
110                                        "Manual intervention is required: {}",
111                                        eth_tx_hash
112                                    )));
113                                }
114                                bridge_event
115                            }
116                            // This only happens when solidity code does not align with rust code.
117                            // When this happens in production, there is a risk of stuck bridge
118                            // transfers. We log error here.
119                            // TODO: add metrics and alert
120                            Err(e) => {
121                                return Err(BridgeError::Generic(format!(
122                                    "Manual intervention is required. Failed to convert TokensDepositedFilter log to EthToIotaTokenBridgeV1. This indicates incorrect parameters or a bug in the code: {:?}. Err: {:?}",
123                                    event, e
124                                )));
125                            }
126                        };
127
128                        Some(BridgeAction::EthToIotaBridgeAction(EthToIotaBridgeAction {
129                            eth_tx_hash,
130                            eth_event_index,
131                            eth_bridge_event: bridge_event,
132                        }))
133                    }
134                    EthIotaBridgeEvents::TokensClaimedFilter(_event) => None,
135                    EthIotaBridgeEvents::PausedFilter(_event) => None,
136                    EthIotaBridgeEvents::UnpausedFilter(_event) => None,
137                    EthIotaBridgeEvents::UpgradedFilter(_event) => None,
138                    EthIotaBridgeEvents::InitializedFilter(_event) => None,
139                }
140            }
141            EthBridgeEvent::EthBridgeCommitteeEvents(event) => match event {
142                EthBridgeCommitteeEvents::BlocklistUpdatedFilter(_event) => None,
143                EthBridgeCommitteeEvents::InitializedFilter(_event) => None,
144                EthBridgeCommitteeEvents::UpgradedFilter(_event) => None,
145            },
146            EthBridgeEvent::EthBridgeLimiterEvents(event) => match event {
147                EthBridgeLimiterEvents::LimitUpdatedFilter(_event) => None,
148                EthBridgeLimiterEvents::InitializedFilter(_event) => None,
149                EthBridgeLimiterEvents::UpgradedFilter(_event) => None,
150                EthBridgeLimiterEvents::HourlyTransferAmountUpdatedFilter(_event) => None,
151                EthBridgeLimiterEvents::OwnershipTransferredFilter(_event) => None,
152            },
153            EthBridgeEvent::EthBridgeConfigEvents(event) => match event {
154                EthBridgeConfigEvents::InitializedFilter(_event) => None,
155                EthBridgeConfigEvents::UpgradedFilter(_event) => None,
156                EthBridgeConfigEvents::TokenAddedFilter(_event) => None,
157                EthBridgeConfigEvents::TokenPriceUpdatedFilter(_event) => None,
158            },
159            EthBridgeEvent::EthCommitteeUpgradeableContractEvents(event) => match event {
160                EthCommitteeUpgradeableContractEvents::InitializedFilter(_event) => None,
161                EthCommitteeUpgradeableContractEvents::UpgradedFilter(_event) => None,
162            },
163        })
164    }
165}
166
167/// The event emitted when tokens are deposited into the bridge on Ethereum.
168/// Sanity checked version of TokensDepositedFilter
169#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
170pub struct EthToIotaTokenBridgeV1 {
171    pub nonce: u64,
172    pub iota_chain_id: BridgeChainId,
173    pub eth_chain_id: BridgeChainId,
174    pub iota_address: IotaAddress,
175    pub eth_address: EthAddress,
176    pub token_id: u8,
177    pub iota_adjusted_amount: u64,
178}
179
180impl TryFrom<&TokensDepositedFilter> for EthToIotaTokenBridgeV1 {
181    type Error = BridgeError;
182    fn try_from(event: &TokensDepositedFilter) -> BridgeResult<Self> {
183        Ok(Self {
184            nonce: event.nonce,
185            iota_chain_id: BridgeChainId::try_from(event.destination_chain_id)?,
186            eth_chain_id: BridgeChainId::try_from(event.source_chain_id)?,
187            iota_address: IotaAddress::from_bytes(event.recipient_address.as_ref())?,
188            eth_address: event.sender_address,
189            token_id: event.token_id,
190            iota_adjusted_amount: event.iota_adjusted_amount,
191        })
192    }
193}
194
195////////////////////////////////////////////////////////////////////////
196//                        Eth Message Conversion                      //
197////////////////////////////////////////////////////////////////////////
198
199impl From<IotaToEthBridgeAction> for eth_iota_bridge::Message {
200    fn from(action: IotaToEthBridgeAction) -> Self {
201        eth_iota_bridge::Message {
202            message_type: BridgeActionType::TokenTransfer as u8,
203            version: TOKEN_TRANSFER_MESSAGE_VERSION,
204            nonce: action.iota_bridge_event.nonce,
205            chain_id: action.iota_bridge_event.iota_chain_id as u8,
206            payload: action.as_payload_bytes().into(),
207        }
208    }
209}
210
211impl From<ParsedTokenTransferMessage> for eth_iota_bridge::Message {
212    fn from(parsed_message: ParsedTokenTransferMessage) -> Self {
213        eth_iota_bridge::Message {
214            message_type: BridgeActionType::TokenTransfer as u8,
215            version: parsed_message.message_version,
216            nonce: parsed_message.seq_num,
217            chain_id: parsed_message.source_chain as u8,
218            payload: parsed_message.payload.into(),
219        }
220    }
221}
222
223impl From<EmergencyAction> for eth_iota_bridge::Message {
224    fn from(action: EmergencyAction) -> Self {
225        eth_iota_bridge::Message {
226            message_type: BridgeActionType::EmergencyButton as u8,
227            version: EMERGENCY_BUTTON_MESSAGE_VERSION,
228            nonce: action.nonce,
229            chain_id: action.chain_id as u8,
230            payload: action.as_payload_bytes().into(),
231        }
232    }
233}
234
235impl From<BlocklistCommitteeAction> for eth_bridge_committee::Message {
236    fn from(action: BlocklistCommitteeAction) -> Self {
237        eth_bridge_committee::Message {
238            message_type: BridgeActionType::UpdateCommitteeBlocklist as u8,
239            version: COMMITTEE_BLOCKLIST_MESSAGE_VERSION,
240            nonce: action.nonce,
241            chain_id: action.chain_id as u8,
242            payload: action.as_payload_bytes().into(),
243        }
244    }
245}
246
247impl From<LimitUpdateAction> for eth_bridge_limiter::Message {
248    fn from(action: LimitUpdateAction) -> Self {
249        eth_bridge_limiter::Message {
250            message_type: BridgeActionType::LimitUpdate as u8,
251            version: LIMIT_UPDATE_MESSAGE_VERSION,
252            nonce: action.nonce,
253            chain_id: action.chain_id as u8,
254            payload: action.as_payload_bytes().into(),
255        }
256    }
257}
258
259impl From<AssetPriceUpdateAction> for eth_bridge_config::Message {
260    fn from(action: AssetPriceUpdateAction) -> Self {
261        eth_bridge_config::Message {
262            message_type: BridgeActionType::AssetPriceUpdate as u8,
263            version: ASSET_PRICE_UPDATE_MESSAGE_VERSION,
264            nonce: action.nonce,
265            chain_id: action.chain_id as u8,
266            payload: action.as_payload_bytes().into(),
267        }
268    }
269}
270
271impl From<AddTokensOnEvmAction> for eth_bridge_config::Message {
272    fn from(action: AddTokensOnEvmAction) -> Self {
273        eth_bridge_config::Message {
274            message_type: BridgeActionType::AddTokensOnEvm as u8,
275            version: ADD_TOKENS_ON_EVM_MESSAGE_VERSION,
276            nonce: action.nonce,
277            chain_id: action.chain_id as u8,
278            payload: action.as_payload_bytes().into(),
279        }
280    }
281}
282
283impl From<EvmContractUpgradeAction> for eth_committee_upgradeable_contract::Message {
284    fn from(action: EvmContractUpgradeAction) -> Self {
285        eth_committee_upgradeable_contract::Message {
286            message_type: BridgeActionType::EvmContractUpgrade as u8,
287            version: EVM_CONTRACT_UPGRADE_MESSAGE_VERSION,
288            nonce: action.nonce,
289            chain_id: action.chain_id as u8,
290            payload: action.as_payload_bytes().into(),
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use std::str::FromStr;
298
299    use ethers::types::TxHash;
300    use fastcrypto::encoding::{Encoding, Hex};
301    use hex_literal::hex;
302    use iota_types::{bridge::TOKEN_ID_ETH, crypto::ToFromBytes};
303
304    use super::*;
305    use crate::{
306        crypto::BridgeAuthorityPublicKeyBytes,
307        types::{BlocklistType, EmergencyActionType},
308    };
309
310    #[test]
311    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
312    fn test_eth_message_conversion_emergency_action_regression() -> anyhow::Result<()> {
313        telemetry_subscribers::init_for_testing();
314
315        let action = EmergencyAction {
316            nonce: 2,
317            chain_id: BridgeChainId::EthSepolia,
318            action_type: EmergencyActionType::Pause,
319        };
320        let message: eth_iota_bridge::Message = action.into();
321        assert_eq!(
322            message,
323            eth_iota_bridge::Message {
324                message_type: BridgeActionType::EmergencyButton as u8,
325                version: EMERGENCY_BUTTON_MESSAGE_VERSION,
326                nonce: 2,
327                chain_id: BridgeChainId::EthSepolia as u8,
328                payload: vec![0].into(),
329            }
330        );
331        Ok(())
332    }
333
334    #[test]
335    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
336    fn test_eth_message_conversion_update_blocklist_action_regression() -> anyhow::Result<()> {
337        telemetry_subscribers::init_for_testing();
338        let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
339            &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
340                .unwrap(),
341        )
342        .unwrap();
343        let action = BlocklistCommitteeAction {
344            nonce: 0,
345            chain_id: BridgeChainId::EthSepolia,
346            blocklist_type: BlocklistType::Blocklist,
347            members_to_update: vec![pub_key_bytes],
348        };
349        let message: eth_bridge_committee::Message = action.into();
350        assert_eq!(
351            message,
352            eth_bridge_committee::Message {
353                message_type: BridgeActionType::UpdateCommitteeBlocklist as u8,
354                version: COMMITTEE_BLOCKLIST_MESSAGE_VERSION,
355                nonce: 0,
356                chain_id: BridgeChainId::EthSepolia as u8,
357                payload: Hex::decode("000168b43fd906c0b8f024a18c56e06744f7c6157c65")
358                    .unwrap()
359                    .into(),
360            }
361        );
362        Ok(())
363    }
364
365    #[test]
366    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
367    fn test_eth_message_conversion_update_limit_action_regression() -> anyhow::Result<()> {
368        telemetry_subscribers::init_for_testing();
369        let action = LimitUpdateAction {
370            nonce: 2,
371            chain_id: BridgeChainId::EthSepolia,
372            sending_chain_id: BridgeChainId::IotaTestnet,
373            new_usd_limit: 4200000,
374        };
375        let message: eth_bridge_limiter::Message = action.into();
376        assert_eq!(
377            message,
378            eth_bridge_limiter::Message {
379                message_type: BridgeActionType::LimitUpdate as u8,
380                version: LIMIT_UPDATE_MESSAGE_VERSION,
381                nonce: 2,
382                chain_id: BridgeChainId::EthSepolia as u8,
383                payload: Hex::decode("010000000000401640").unwrap().into(),
384            }
385        );
386        Ok(())
387    }
388
389    #[test]
390    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
391    fn test_eth_message_conversion_contract_upgrade_action_regression() -> anyhow::Result<()> {
392        telemetry_subscribers::init_for_testing();
393        let action = EvmContractUpgradeAction {
394            nonce: 2,
395            chain_id: BridgeChainId::EthSepolia,
396            proxy_address: EthAddress::repeat_byte(1),
397            new_impl_address: EthAddress::repeat_byte(2),
398            call_data: Vec::from("deadbeef"),
399        };
400        let message: eth_committee_upgradeable_contract::Message = action.into();
401        assert_eq!(
402            message,
403            eth_committee_upgradeable_contract::Message {
404                message_type: BridgeActionType::EvmContractUpgrade as u8,
405                version: EVM_CONTRACT_UPGRADE_MESSAGE_VERSION,
406                nonce: 2,
407                chain_id: BridgeChainId::EthSepolia as u8,
408                payload: Hex::decode("0x00000000000000000000000001010101010101010101010101010101010101010000000000000000000000000202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000086465616462656566000000000000000000000000000000000000000000000000").unwrap().into(),
409            }
410        );
411        Ok(())
412    }
413
414    #[test]
415    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
416    fn test_eth_message_conversion_update_price_action_regression() -> anyhow::Result<()> {
417        telemetry_subscribers::init_for_testing();
418        let action = AssetPriceUpdateAction {
419            nonce: 2,
420            chain_id: BridgeChainId::EthSepolia,
421            token_id: TOKEN_ID_ETH,
422            new_usd_price: 80000000,
423        };
424        let message: eth_bridge_config::Message = action.into();
425        assert_eq!(
426            message,
427            eth_bridge_config::Message {
428                message_type: BridgeActionType::AssetPriceUpdate as u8,
429                version: ASSET_PRICE_UPDATE_MESSAGE_VERSION,
430                nonce: 2,
431                chain_id: BridgeChainId::EthSepolia as u8,
432                payload: Hex::decode("020000000004c4b400").unwrap().into(),
433            }
434        );
435        Ok(())
436    }
437
438    #[test]
439    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
440    fn test_eth_message_conversion_add_tokens_on_evm_action_regression() -> anyhow::Result<()> {
441        let action = AddTokensOnEvmAction {
442            nonce: 5,
443            chain_id: BridgeChainId::EthCustom,
444            native: true,
445            token_ids: vec![99, 100, 101],
446            token_addresses: vec![
447                EthAddress::repeat_byte(1),
448                EthAddress::repeat_byte(2),
449                EthAddress::repeat_byte(3),
450            ],
451            token_iota_decimals: vec![5, 6, 7],
452            token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
453        };
454        let message: eth_bridge_config::Message = action.into();
455        assert_eq!(
456            message,
457            eth_bridge_config::Message {
458                message_type: BridgeActionType::AddTokensOnEvm as u8,
459                version: ADD_TOKENS_ON_EVM_MESSAGE_VERSION,
460                nonce: 5,
461                chain_id: BridgeChainId::EthCustom as u8,
462                payload: Hex::decode("0103636465030101010101010101010101010101010101010101020202020202020202020202020202020202020203030303030303030303030303030303030303030305060703000000003b9aca00000000007735940000000000b2d05e00").unwrap().into(),
463            }
464        );
465        Ok(())
466    }
467
468    #[test]
469    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
470    fn test_token_deposit_eth_log_to_iota_bridge_event_regression() -> anyhow::Result<()> {
471        telemetry_subscribers::init_for_testing();
472        let tx_hash = TxHash::random();
473        let action = EthLog {
474            block_number: 33,
475            tx_hash,
476            log_index_in_tx: 1,
477            log: Log {
478                address: EthAddress::repeat_byte(1),
479                topics: vec![
480                    hex!("a0f1d54820817ede8517e70a3d0a9197c015471c5360d2119b759f0359858ce6").into(),
481                    hex!("000000000000000000000000000000000000000000000000000000000000000c").into(),
482                    hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
483                    hex!("0000000000000000000000000000000000000000000000000000000000000002").into(),
484                ],
485                data: ethers::types::Bytes::from(
486                    Hex::decode("0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000fa56ea0000000000000000000000000014dc79964da2c08b23698b3d3cc7ca32193d9955000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000203b1eb23133e94d08d0da9303cfd38e7d4f8f6951f235daa62cd64ea5b6d96d77").unwrap(),
487                ),
488                block_hash: None,
489                block_number: None,
490                transaction_hash: Some(tx_hash),
491                transaction_index: Some(ethers::types::U64::from(0)),
492                log_index: Some(ethers::types::U256::from(1)),
493                transaction_log_index: None,
494                log_type: None,
495                removed: Some(false),
496            }
497        };
498        let event = EthBridgeEvent::try_from_eth_log(&action).unwrap();
499        assert_eq!(
500            event,
501            EthBridgeEvent::EthIotaBridgeEvents(EthIotaBridgeEvents::TokensDepositedFilter(
502                TokensDepositedFilter {
503                    source_chain_id: 12,
504                    nonce: 0,
505                    destination_chain_id: 2,
506                    token_id: 2,
507                    iota_adjusted_amount: 4200000000,
508                    sender_address: EthAddress::from_str(
509                        "0x14dc79964da2c08b23698b3d3cc7ca32193d9955"
510                    )
511                    .unwrap(),
512                    recipient_address: ethers::types::Bytes::from(
513                        Hex::decode(
514                            "0x3b1eb23133e94d08d0da9303cfd38e7d4f8f6951f235daa62cd64ea5b6d96d77"
515                        )
516                        .unwrap(),
517                    ),
518                }
519            ))
520        );
521        Ok(())
522    }
523
524    #[test]
525    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
526    fn test_0_iota_amount_conversion_for_eth_event() {
527        let e = EthBridgeEvent::EthIotaBridgeEvents(EthIotaBridgeEvents::TokensDepositedFilter(
528            TokensDepositedFilter {
529                source_chain_id: BridgeChainId::EthSepolia as u8,
530                nonce: 0,
531                destination_chain_id: BridgeChainId::IotaTestnet as u8,
532                token_id: 2,
533                iota_adjusted_amount: 1,
534                sender_address: EthAddress::random(),
535                recipient_address: ethers::types::Bytes::from(
536                    IotaAddress::random_for_testing_only().to_vec(),
537                ),
538            },
539        ));
540        assert!(
541            e.try_into_bridge_action(TxHash::random(), 0)
542                .unwrap()
543                .is_some()
544        );
545
546        let e = EthBridgeEvent::EthIotaBridgeEvents(EthIotaBridgeEvents::TokensDepositedFilter(
547            TokensDepositedFilter {
548                source_chain_id: BridgeChainId::EthSepolia as u8,
549                nonce: 0,
550                destination_chain_id: BridgeChainId::IotaTestnet as u8,
551                token_id: 2,
552                iota_adjusted_amount: 0, // <------------
553                sender_address: EthAddress::random(),
554                recipient_address: ethers::types::Bytes::from(
555                    IotaAddress::random_for_testing_only().to_vec(),
556                ),
557            },
558        ));
559        match e.try_into_bridge_action(TxHash::random(), 0).unwrap_err() {
560            BridgeError::ZeroValueBridgeTransfer(_) => {}
561            e => panic!("Unexpected error: {:?}", e),
562        }
563    }
564}