sui_bridge/sui_bridge_watchdog/
eth_vault_balance.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::abi::EthERC20::EthERC20Instance;
5use crate::sui_bridge_watchdog::Observable;
6use crate::utils::EthProvider;
7use alloy::primitives::{Address as EthAddress, U256};
8use async_trait::async_trait;
9use prometheus::IntGauge;
10use tokio::time::Duration;
11use tracing::{error, info};
12
13#[derive(Debug)]
14pub enum VaultAsset {
15    WETH,
16    USDT,
17    WBTC,
18    LBTC,
19}
20
21pub struct EthereumVaultBalance {
22    coin_contract: EthERC20Instance<EthProvider>,
23    asset: VaultAsset,
24    decimals: u8,
25    vault_address: EthAddress,
26    metric: IntGauge,
27}
28
29impl EthereumVaultBalance {
30    pub async fn new(
31        provider: EthProvider,
32        vault_address: EthAddress,
33        coin_address: EthAddress, // for now this only support one coin which is WETH
34        asset: VaultAsset,
35        metric: IntGauge,
36    ) -> anyhow::Result<Self> {
37        let coin_contract = EthERC20Instance::new(coin_address, provider);
38        let decimals = coin_contract
39            .decimals()
40            .call()
41            .await
42            .map_err(|e| anyhow::anyhow!("Failed to get decimals from token contract: {e}"))?;
43        Ok(Self {
44            coin_contract,
45            vault_address,
46            decimals,
47            asset,
48            metric,
49        })
50    }
51}
52
53#[async_trait]
54impl Observable for EthereumVaultBalance {
55    fn name(&self) -> &str {
56        "EthereumVaultBalance"
57    }
58    async fn observe_and_report(&self) {
59        let balance: Result<U256, alloy::contract::Error> = self
60            .coin_contract
61            .balanceOf(self.vault_address)
62            .call()
63            .await;
64        match balance {
65            Ok(balance) => {
66                // Why downcasting is safe:
67                // 1. On Ethereum we only take the first 8 decimals into account,
68                // meaning the trailing 10 digits can be ignored. For other assets,
69                // we will also assume this max level of precision for metrics purposes.
70                // 2. i64::MAX is 9_223_372_036_854_775_807, with 8 decimal places is
71                // 92_233_720_368. We likely won't see any balance higher than this
72                // in the next 12 months.
73                // For USDT, for example, this will be 10^6 - 8 = 10^(-2) = 0.01,
74                // therefore we will add 2 zeroes of precision.
75                let normalized_balance: U256 = match self.decimals.checked_sub(8) {
76                    // In this case, there are more decimals than needed, so we need to
77                    // remove trailing decimals.
78                    Some(delta) if delta > 0 => balance
79                        .checked_div(U256::from(10).pow(U256::from(delta)))
80                        .expect("Division by zero should be impossible here"),
81                    // In this case, there are fewer decimals than needed, so we need to
82                    // add zeroes.
83                    None => {
84                        let delta = 8 - self.decimals;
85                        balance
86                            .checked_mul(U256::from(10).pow(U256::from(delta)))
87                            .expect("Integer overflow")
88                    }
89                    // in this case, the token contract has the target precision
90                    // so we don't need to do anything.
91                    Some(_) => balance,
92                };
93                self.metric.set(normalized_balance.to::<i64>());
94
95                info!("{:?} Vault Balance: {:?}", self.asset, normalized_balance,);
96            }
97            Err(e) => {
98                error!("Error getting balance from vault: {:?}", e);
99            }
100        }
101    }
102
103    fn interval(&self) -> Duration {
104        Duration::from_secs(10)
105    }
106}