|
| 1 | +use alloy::primitives::{Address, U256}; |
| 2 | +use alloy::providers::ProviderBuilder; |
| 3 | +use alloy::sol; |
| 4 | +use alloy::sol_types::SolCall; |
| 5 | +use async_trait::async_trait; |
| 6 | + |
| 7 | +use defi_core::error::{DefiError, Result}; |
| 8 | +use defi_core::traits::Lending; |
| 9 | +use defi_core::types::*; |
| 10 | + |
| 11 | +sol! { |
| 12 | + #[sol(rpc)] |
| 13 | + interface IComet { |
| 14 | + function getUtilization() external view returns (uint256); |
| 15 | + function getSupplyRate(uint256 utilization) external view returns (uint64); |
| 16 | + function getBorrowRate(uint256 utilization) external view returns (uint64); |
| 17 | + function totalSupply() external view returns (uint256); |
| 18 | + function totalBorrow() external view returns (uint256); |
| 19 | + function supply(address asset, uint256 amount) external; |
| 20 | + function withdraw(address asset, uint256 amount) external; |
| 21 | + } |
| 22 | +} |
| 23 | + |
| 24 | +/// Seconds per year for rate conversion |
| 25 | +const SECONDS_PER_YEAR: f64 = 365.25 * 24.0 * 3600.0; |
| 26 | + |
| 27 | +pub struct CompoundV3 { |
| 28 | + name: String, |
| 29 | + comet: Address, |
| 30 | + rpc_url: Option<String>, |
| 31 | +} |
| 32 | + |
| 33 | +impl CompoundV3 { |
| 34 | + pub fn from_contracts( |
| 35 | + name: String, |
| 36 | + contracts: &std::collections::HashMap<String, Address>, |
| 37 | + rpc_url: Option<String>, |
| 38 | + ) -> Result<Self> { |
| 39 | + let comet = contracts |
| 40 | + .get("comet_usdc") |
| 41 | + .or(contracts.get("comet")) |
| 42 | + .or(contracts.get("comet_weth")) |
| 43 | + .copied() |
| 44 | + .ok_or_else(|| { |
| 45 | + DefiError::ContractError("Missing 'comet_usdc' or 'comet' address".to_string()) |
| 46 | + })?; |
| 47 | + Ok(Self { |
| 48 | + name, |
| 49 | + comet, |
| 50 | + rpc_url, |
| 51 | + }) |
| 52 | + } |
| 53 | + |
| 54 | + fn rpc_url(&self) -> Result<url::Url> { |
| 55 | + self.rpc_url |
| 56 | + .as_ref() |
| 57 | + .ok_or_else(|| DefiError::RpcError("No RPC URL configured".to_string()))? |
| 58 | + .parse() |
| 59 | + .map_err(|e| DefiError::RpcError(format!("Invalid RPC URL: {e}"))) |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +#[async_trait] |
| 64 | +impl Lending for CompoundV3 { |
| 65 | + fn name(&self) -> &str { |
| 66 | + &self.name |
| 67 | + } |
| 68 | + |
| 69 | + async fn build_supply(&self, params: SupplyParams) -> Result<DeFiTx> { |
| 70 | + let call = IComet::supplyCall { |
| 71 | + asset: params.asset, |
| 72 | + amount: params.amount, |
| 73 | + }; |
| 74 | + Ok(DeFiTx { |
| 75 | + description: format!("[{}] Supply {} to Comet", self.name, params.amount), |
| 76 | + to: self.comet, |
| 77 | + data: call.abi_encode().into(), |
| 78 | + value: U256::ZERO, |
| 79 | + gas_estimate: Some(300_000), |
| 80 | + }) |
| 81 | + } |
| 82 | + |
| 83 | + async fn build_borrow(&self, params: BorrowParams) -> Result<DeFiTx> { |
| 84 | + let call = IComet::withdrawCall { |
| 85 | + asset: params.asset, |
| 86 | + amount: params.amount, |
| 87 | + }; |
| 88 | + Ok(DeFiTx { |
| 89 | + description: format!("[{}] Borrow {} from Comet", self.name, params.amount), |
| 90 | + to: self.comet, |
| 91 | + data: call.abi_encode().into(), |
| 92 | + value: U256::ZERO, |
| 93 | + gas_estimate: Some(350_000), |
| 94 | + }) |
| 95 | + } |
| 96 | + |
| 97 | + async fn build_repay(&self, params: RepayParams) -> Result<DeFiTx> { |
| 98 | + let call = IComet::supplyCall { |
| 99 | + asset: params.asset, |
| 100 | + amount: params.amount, |
| 101 | + }; |
| 102 | + Ok(DeFiTx { |
| 103 | + description: format!("[{}] Repay {} to Comet", self.name, params.amount), |
| 104 | + to: self.comet, |
| 105 | + data: call.abi_encode().into(), |
| 106 | + value: U256::ZERO, |
| 107 | + gas_estimate: Some(300_000), |
| 108 | + }) |
| 109 | + } |
| 110 | + |
| 111 | + async fn build_withdraw(&self, params: WithdrawParams) -> Result<DeFiTx> { |
| 112 | + let call = IComet::withdrawCall { |
| 113 | + asset: params.asset, |
| 114 | + amount: params.amount, |
| 115 | + }; |
| 116 | + Ok(DeFiTx { |
| 117 | + description: format!("[{}] Withdraw from Comet", self.name), |
| 118 | + to: self.comet, |
| 119 | + data: call.abi_encode().into(), |
| 120 | + value: U256::ZERO, |
| 121 | + gas_estimate: Some(250_000), |
| 122 | + }) |
| 123 | + } |
| 124 | + |
| 125 | + async fn get_rates(&self, _asset: Address) -> Result<LendingRates> { |
| 126 | + let url = self.rpc_url()?; |
| 127 | + let provider = ProviderBuilder::new().connect_http(url); |
| 128 | + let comet = IComet::new(self.comet, &provider); |
| 129 | + |
| 130 | + let utilization = comet.getUtilization().call().await.map_err(|e| { |
| 131 | + DefiError::RpcError(format!("[{}] getUtilization failed: {e}", self.name)) |
| 132 | + })?; |
| 133 | + |
| 134 | + let supply_rate = comet.getSupplyRate(utilization).call().await.map_err(|e| { |
| 135 | + DefiError::RpcError(format!("[{}] getSupplyRate failed: {e}", self.name)) |
| 136 | + })?; |
| 137 | + |
| 138 | + let borrow_rate = comet.getBorrowRate(utilization).call().await.map_err(|e| { |
| 139 | + DefiError::RpcError(format!("[{}] getBorrowRate failed: {e}", self.name)) |
| 140 | + })?; |
| 141 | + |
| 142 | + let total_supply = comet.totalSupply().call().await.unwrap_or(U256::ZERO); |
| 143 | + let total_borrow = comet.totalBorrow().call().await.unwrap_or(U256::ZERO); |
| 144 | + |
| 145 | + // Comet rates are per-second, scaled by 1e18 |
| 146 | + let supply_per_sec = supply_rate as f64 / 1e18; |
| 147 | + let borrow_per_sec = borrow_rate as f64 / 1e18; |
| 148 | + let supply_apy = supply_per_sec * SECONDS_PER_YEAR * 100.0; |
| 149 | + let borrow_apy = borrow_per_sec * SECONDS_PER_YEAR * 100.0; |
| 150 | + let util_pct = utilization.to::<u128>() as f64 / 1e18 * 100.0; |
| 151 | + |
| 152 | + Ok(LendingRates { |
| 153 | + protocol: self.name.clone(), |
| 154 | + asset: _asset, |
| 155 | + supply_apy, |
| 156 | + borrow_variable_apy: borrow_apy, |
| 157 | + borrow_stable_apy: None, |
| 158 | + utilization: util_pct, |
| 159 | + total_supply, |
| 160 | + total_borrow, |
| 161 | + }) |
| 162 | + } |
| 163 | + |
| 164 | + async fn get_user_position(&self, _user: Address) -> Result<UserPosition> { |
| 165 | + Err(DefiError::Unsupported(format!( |
| 166 | + "[{}] User position requires querying Comet balanceOf + borrowBalanceOf", |
| 167 | + self.name |
| 168 | + ))) |
| 169 | + } |
| 170 | +} |
0 commit comments