Skip to content

Commit

Permalink
funds-manager: Change external deposit/withdraw target to hot wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Jul 30, 2024
1 parent a338b56 commit 8168d81
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 33 deletions.
11 changes: 9 additions & 2 deletions funds-manager/funds-manager-server/src/custody_client/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ impl CustodyClient {
/// Get the deposit address for the given mint
pub(crate) async fn get_deposit_address(
&self,
mint: &str,
source: DepositWithdrawSource,
) -> Result<String, FundsManagerError> {
let vault_name = source.vault_name();
self.get_deposit_address_by_vault_name(mint, vault_name).await
self.get_deposit_address_by_vault_name(vault_name).await
}

/// Get the deposit address given a vault name
pub(crate) async fn get_deposit_address_by_vault_name(
&self,
vault_name: &str,
) -> Result<String, FundsManagerError> {
self.get_hot_wallet_by_vault(vault_name).await.map(|w| w.address)
}

/// Get the deposit address given a vault name
pub(crate) async fn get_fireblocks_deposit_address(
&self,
mint: &str,
vault_name: &str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ impl CustodyClient {
let wallet = LocalWallet::from_str(&secret_value).map_err(FundsManagerError::parse)?;

// 3. Look up the vault deposit address
let deposit_address =
self.get_deposit_address_by_vault_name(mint, &hot_wallet.vault).await?;
let deposit_address = self.get_fireblocks_deposit_address(mint, &hot_wallet.vault).await?;

// 4. Transfer the tokens
let receipt = self.erc20_transfer(mint, &deposit_address, amount, wallet).await?;
Expand All @@ -114,15 +113,16 @@ impl CustodyClient {
// Fetch the wallet info, then withdraw
let wallet = self.get_hot_wallet_by_vault(vault).await?;
let source = DepositWithdrawSource::from_vault_name(vault)?;
self.withdraw_with_token_addr(source, &wallet.address, mint, amount).await
let symbol = self.get_erc20_token_symbol(mint).await?;
self.withdraw_from_fireblocks(source, &wallet.address, &symbol, amount).await
}

// ------------
// | Handlers |
// ------------

/// The secret name for a hot wallet
fn hot_wallet_secret_name(address: &str) -> String {
pub(crate) fn hot_wallet_secret_name(address: &str) -> String {
format!("hot-wallet-{address}")
}

Expand Down
66 changes: 64 additions & 2 deletions funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ mod queries;
pub mod withdraw;

use aws_config::SdkConfig as AwsConfig;
use bigdecimal::{BigDecimal, ToPrimitive};

Check failure on line 9 in funds-manager/funds-manager-server/src/custody_client/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

unused imports: `BigDecimal`, `ToPrimitive`

error: unused imports: `BigDecimal`, `ToPrimitive` --> funds-manager/funds-manager-server/src/custody_client/mod.rs:9:18 | 9 | use bigdecimal::{BigDecimal, ToPrimitive}; | ^^^^^^^^^^ ^^^^^^^^^^^ | = note: `-D unused-imports` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(unused_imports)]`
use ethers::middleware::SignerMiddleware;
use ethers::prelude::abigen;
use ethers::providers::{Http, Provider};
use ethers::providers::{Http, Middleware, Provider};
use ethers::signers::{LocalWallet, Signer};
use ethers::types::{Address, TransactionReceipt};
use ethers::types::{Address, TransactionReceipt, TransactionRequest, U256};
use ethers::utils::format_units;
use fireblocks_sdk::types::Transaction;
use fireblocks_sdk::{
types::{Account as FireblocksAccount, AccountAsset},
Expand Down Expand Up @@ -178,6 +180,43 @@ impl CustodyClient {
.map_err(err_str!(FundsManagerError::Arbitrum))
}

/// Get the native token balance of an address
pub(crate) async fn get_ether_balance(&self, address: &str) -> Result<f64, FundsManagerError> {
let provider = self.get_rpc_provider()?;
let client = Arc::new(provider);
let address = Address::from_str(address).map_err(FundsManagerError::parse)?;
let balance =
client.get_balance(address, None).await.map_err(FundsManagerError::arbitrum)?;

// Convert U256 to f64
let balance_str = format_units(balance, "ether").map_err(FundsManagerError::parse)?;
balance_str.parse::<f64>().map_err(FundsManagerError::parse)
}

/// Transfer ether from the given wallet
pub(crate) async fn transfer_ether(
&self,
to: &str,
amount: f64,
wallet: LocalWallet,
) -> Result<TransactionReceipt, FundsManagerError> {
let wallet = wallet.with_chain_id(self.chain_id);
let provider = self.get_rpc_provider()?;
let client = SignerMiddleware::new(provider, wallet);

let to = Address::from_str(to).map_err(FundsManagerError::parse)?;
let amount = ethers::utils::parse_units(amount.to_string(), "ether")
.map_err(FundsManagerError::parse)?;

let tx = TransactionRequest::new().to(to).value(U256::from(amount));
let pending_tx =
client.send_transaction(tx, None).await.map_err(FundsManagerError::arbitrum)?;
pending_tx
.await
.map_err(FundsManagerError::arbitrum)?
.ok_or_else(|| FundsManagerError::arbitrum("Transaction failed".to_string()))
}

/// Get the symbol for an ERC20 token at the given address
pub(self) async fn get_erc20_token_symbol(
&self,
Expand All @@ -192,6 +231,29 @@ impl CustodyClient {
erc20.symbol().call().await.map_err(FundsManagerError::arbitrum)
}

/// Get the erc20 balance of an address
pub(crate) async fn get_erc20_balance(
&self,
token_address: &str,
address: &str,
) -> Result<f64, FundsManagerError> {
// Setup the provider
let token_address = Address::from_str(token_address).map_err(FundsManagerError::parse)?;
let address = Address::from_str(address).map_err(FundsManagerError::parse)?;
let provider = self.get_rpc_provider()?;
let client = Arc::new(provider);
let erc20 = ERC20::new(token_address, client);

// Fetch the balance and correct for the ERC20 decimal precision
let decimals = erc20.decimals().call().await.map_err(FundsManagerError::arbitrum)? as u32;
let balance =
erc20.balance_of(address).call().await.map_err(FundsManagerError::arbitrum)?;
let bal_str = format_units(balance, decimals).map_err(FundsManagerError::parse)?;
let bal_f64 = bal_str.parse::<f64>().map_err(FundsManagerError::parse)?;

Ok(bal_f64)
}

/// Perform an erc20 transfer
pub(crate) async fn erc20_transfer(
&self,
Expand Down
62 changes: 56 additions & 6 deletions funds-manager/funds-manager-server/src/custody_client/withdraw.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
use crate::error::FundsManagerError;
use std::str::FromStr;

use crate::{error::FundsManagerError, helpers::get_secret};
use bigdecimal::{BigDecimal, FromPrimitive};
use ethers::signers::LocalWallet;
use fireblocks_sdk::types::TransactionStatus;
use tracing::info;

use super::{CustodyClient, DepositWithdrawSource};

impl CustodyClient {
/// Withdraw gas from custody
pub(crate) async fn withdraw_with_token_addr(
/// Withdraw from hot wallet custody with a provided token address
pub(crate) async fn withdraw_from_hot_wallet(
&self,
source: DepositWithdrawSource,
destination_address: &str,
token_address: &str,
amount: f64,
) -> Result<(), FundsManagerError> {
let symbol = self.get_erc20_token_symbol(token_address).await?;
self.withdraw(source, destination_address, &symbol, amount).await
// Find the wallet for the given destination and check its balance
let wallet = self.get_hot_wallet_by_vault(source.vault_name()).await?;
let bal = self.get_erc20_balance(token_address, &wallet.address).await?;
if bal < amount {
return Err(FundsManagerError::Custom("Insufficient balance".to_string()));
}

// Fetch the wallet private key
let secret_name = Self::hot_wallet_secret_name(&wallet.address);
let private_key = get_secret(&secret_name, &self.aws_config).await?;
let wallet =
LocalWallet::from_str(private_key.as_str()).map_err(FundsManagerError::parse)?;

// Execute the erc20 transfer
let tx = self.erc20_transfer(token_address, destination_address, amount, wallet).await?;
info!(
"Withdrew {amount} {token_address} from hot wallet to {destination_address}. Tx: {:#}",
tx.transaction_hash
);

Ok(())
}

/// Withdraw funds from custody
pub(crate) async fn withdraw(
pub(crate) async fn withdraw_from_fireblocks(
&self,
source: DepositWithdrawSource,
destination_address: &str,
Expand Down Expand Up @@ -69,4 +92,31 @@ impl CustodyClient {

Ok(())
}

/// Withdraw gas
pub(crate) async fn withdraw_gas(
&self,
amount: f64,
to: &str,
) -> Result<(), FundsManagerError> {
// Check the gas wallet's balance
let gas_vault_name = DepositWithdrawSource::Gas.vault_name();
let gas_wallet = self.get_hot_wallet_by_vault(gas_vault_name).await?;
let bal = self.get_ether_balance(&gas_wallet.address).await?;
if bal < amount {
return Err(FundsManagerError::custom("Insufficient balance"));
}

// Fetch the gas wallet's private key
let secret_name = Self::hot_wallet_secret_name(&gas_wallet.address);
let private_key = get_secret(&secret_name, &self.aws_config).await?;
let wallet =
LocalWallet::from_str(private_key.as_str()).map_err(FundsManagerError::parse)?;

// Execute the transfer
let tx = self.transfer_ether(to, amount, wallet).await?;
info!("Withdrew {amount} ETH from gas wallet to {to}. Tx: {:#}", tx.transaction_hash);

Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ impl Indexer {
old_wallet.key_chain.secret_keys.sk_root.as_ref().expect("root key not present");

// Get the deposit address for the fee withdrawal
let deposit_address = self
.custody_client
.get_deposit_address(&mint, DepositWithdrawSource::FeeRedemption)
.await?;
let deposit_address =
self.custody_client.get_deposit_address(DepositWithdrawSource::FeeRedemption).await?;

// Send a withdrawal request to the relayer
let req = Self::build_withdrawal_request(&mint, &deposit_address, &old_wallet)?;
Expand Down
16 changes: 3 additions & 13 deletions funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub(crate) async fn quoter_withdraw_handler(
) -> Result<Json, warp::Rejection> {
server
.custody_client
.withdraw_with_token_addr(
.withdraw_from_hot_wallet(
DepositWithdrawSource::Quoter,
&withdraw_request.address,
&withdraw_request.mint,
Expand All @@ -66,16 +66,11 @@ pub(crate) async fn quoter_withdraw_handler(

/// Handler for retrieving the address to deposit custody funds to
pub(crate) async fn get_deposit_address_handler(
query_params: HashMap<String, String>,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
let mint = query_params.get(MINTS_QUERY_PARAM).ok_or_else(|| {
warp::reject::custom(ApiError::BadRequest("Missing 'mints' query parameter".to_string()))
})?;

let address = server
.custody_client
.get_deposit_address(mint, DepositWithdrawSource::Quoter)
.get_deposit_address(DepositWithdrawSource::Quoter)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
let resp = DepositAddressResponse { address };
Expand All @@ -96,12 +91,7 @@ pub(crate) async fn withdraw_gas_handler(

server
.custody_client
.withdraw(
DepositWithdrawSource::Gas,
&withdraw_request.destination_address,
GAS_ASSET_NAME,
withdraw_request.amount,
)
.withdraw_gas(withdraw_request.amount, &withdraw_request.destination_address)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;

Expand Down
3 changes: 1 addition & 2 deletions funds-manager/funds-manager-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use arbitrum_client::{
use clap::Parser;
use funds_manager_api::WithdrawFundsRequest;
use tracing::{error, warn};
use warp::{filters::query::query, Filter};
use warp::Filter;

use crate::custody_client::CustodyClient;
use crate::error::ApiError;
Expand Down Expand Up @@ -325,7 +325,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
.and(warp::path("custody"))
.and(warp::path("quoters"))
.and(warp::path(GET_DEPOSIT_ADDRESS_ROUTE))
.and(query::<HashMap<String, String>>())
.and(with_server(server.clone()))
.and_then(get_deposit_address_handler);

Expand Down

0 comments on commit 8168d81

Please sign in to comment.