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