sui_bridge/sui_bridge_watchdog/
eth_vault_balance.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::abi::EthERC20;
use crate::metered_eth_provider::MeteredEthHttpProvier;
use crate::sui_bridge_watchdog::Observable;
use async_trait::async_trait;
use ethers::providers::Provider;
use ethers::types::{Address as EthAddress, U256};
use prometheus::IntGauge;
use std::sync::Arc;
use tokio::time::Duration;
use tracing::{error, info};

#[derive(Debug)]
pub enum VaultAsset {
    WETH,
    USDT,
    WBTC,
    LBTC,
}

pub struct EthereumVaultBalance {
    coin_contract: EthERC20<Provider<MeteredEthHttpProvier>>,
    asset: VaultAsset,
    decimals: u8,
    vault_address: EthAddress,
    metric: IntGauge,
}

impl EthereumVaultBalance {
    pub async fn new(
        provider: Arc<Provider<MeteredEthHttpProvier>>,
        vault_address: EthAddress,
        coin_address: EthAddress, // for now this only support one coin which is WETH
        asset: VaultAsset,
        metric: IntGauge,
    ) -> anyhow::Result<Self> {
        let coin_contract = EthERC20::new(coin_address, provider);
        let decimals = coin_contract
            .decimals()
            .call()
            .await
            .map_err(|e| anyhow::anyhow!("Failed to get decimals from token contract: {e}"))?;
        Ok(Self {
            coin_contract,
            vault_address,
            decimals,
            asset,
            metric,
        })
    }
}

#[async_trait]
impl Observable for EthereumVaultBalance {
    fn name(&self) -> &str {
        "EthereumVaultBalance"
    }
    async fn observe_and_report(&self) {
        let balance: Result<
            U256,
            ethers::contract::ContractError<Provider<MeteredEthHttpProvier>>,
        > = self
            .coin_contract
            .balance_of(self.vault_address)
            .call()
            .await;
        match balance {
            Ok(balance) => {
                // Why downcasting is safe:
                // 1. On Ethereum we only take the first 8 decimals into account,
                // meaning the trailing 10 digits can be ignored. For other assets,
                // we will also assume this max level of precision for metrics purposes.
                // 2. i64::MAX is 9_223_372_036_854_775_807, with 8 decimal places is
                // 92_233_720_368. We likely won't see any balance higher than this
                // in the next 12 months.
                // For USDT, for example, this will be 10^6 - 8 = 10^(-2) = 0.01,
                // therefore we will add 2 zeroes of precision.
                let normalized_balance: U256 = match self.decimals.checked_sub(8) {
                    // In this case, there are more decimals than needed, so we need to
                    // remove trailing decimals.
                    Some(delta) if delta > 0 => balance
                        .checked_div(U256::from(10).pow(U256::from(delta)))
                        .expect("Division by zero should be impossible here"),
                    // In this case, there are fewer decimals than needed, so we need to
                    // add zeroes.
                    None => {
                        let delta = 8 - self.decimals;
                        balance
                            .checked_mul(U256::from(10).pow(U256::from(delta)))
                            .expect("Integer overflow")
                    }
                    // in this case, the token contract has the target precision
                    // so we don't need to do anything.
                    Some(_) => balance,
                };
                self.metric.set(normalized_balance.as_u128() as i64);

                info!("{:?} Vault Balance: {:?}", self.asset, normalized_balance,);
            }
            Err(e) => {
                error!("Error getting balance from vault: {:?}", e);
            }
        }
    }

    fn interval(&self) -> Duration {
        Duration::from_secs(10)
    }
}