Skip to content

Commit

Permalink
Merge pull request #16 from renegade-fi/joey/withdraw-fee-balance
Browse files Browse the repository at this point in the history
funds-manager: fee-indexer: Allow withdrawal of fees to custody
  • Loading branch information
joeykraut authored Jul 30, 2024
2 parents 71c143b + 3cea302 commit e30f86c
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 15 deletions.
1 change: 1 addition & 0 deletions funds-manager/funds-manager-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ edition = "2021"
renegade-api = { package = "external-api", workspace = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.117"
uuid = "1.7.1"
13 changes: 13 additions & 0 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use renegade_api::types::ApiWallet;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// --------------
// | Api Routes |
Expand All @@ -27,6 +28,9 @@ pub const WITHDRAW_GAS_ROUTE: &str = "withdraw-gas";
/// The route to get fee wallets
pub const GET_FEE_WALLETS_ROUTE: &str = "get-fee-wallets";

/// The route to withdraw a fee balance
pub const WITHDRAW_FEE_BALANCE_ROUTE: &str = "withdraw-fee-balance";

// -------------
// | Api Types |
// -------------
Expand Down Expand Up @@ -65,3 +69,12 @@ pub struct FeeWalletsResponse {
/// The wallets managed by the funds manager
pub wallets: Vec<ApiWallet>,
}

/// The request body for withdrawing a fee balance
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WithdrawFeeBalanceRequest {
/// The ID of the wallet to withdraw from
pub wallet_id: Uuid,
/// The mint of the asset to withdraw
pub mint: String,
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl DepositWithdrawSource {
pub(crate) fn get_vault_name(&self) -> &str {
match self {
Self::Quoter => "Quoters",
Self::FeeRedemption => unimplemented!("no vault for fee redemption yet"),
Self::FeeRedemption => "Fee Collection",
Self::Gas => "Arbitrum Gas",
}
}
Expand Down
137 changes: 132 additions & 5 deletions funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
//! Fetch the balances of redeemed fees

use crate::custody_client::DepositWithdrawSource;
use crate::db::models::WalletMetadata;
use crate::error::FundsManagerError;
use renegade_api::types::ApiWallet;
use renegade_common::types::wallet::derivation::derive_wallet_keychain;
use arbitrum_client::{conversion::to_contract_external_transfer, helpers::serialize_calldata};
use ethers::{
core::k256::ecdsa::SigningKey,
types::{Signature, U256},
utils::keccak256,
};
use num_bigint::BigUint;
use renegade_api::{http::wallet::WithdrawBalanceRequest, types::ApiWallet};
use renegade_circuit_types::{
keychain::SecretSigningKey,
transfers::{ExternalTransfer, ExternalTransferDirection},
Amount,
};
use renegade_common::types::wallet::{derivation::derive_wallet_keychain, Wallet};
use renegade_util::hex::biguint_from_hex_string;
use uuid::Uuid;

use super::Indexer;

impl Indexer {
// -------------
// | Interface |
// -------------

/// Fetch fee balances for wallets managed by the funds manager
pub async fn fetch_fee_wallets(&mut self) -> Result<Vec<ApiWallet>, FundsManagerError> {
// Query the wallets and fetch from the relayer
Expand All @@ -21,6 +40,36 @@ impl Indexer {
Ok(wallets)
}

/// Withdraw a fee balance for a specific wallet and mint
pub async fn withdraw_fee_balance(
&mut self,
wallet_id: Uuid,
mint: String,
) -> Result<(), FundsManagerError> {
// Fetch the Renegade wallet
let wallet_metadata = self.get_wallet_by_id(&wallet_id).await?;
let api_wallet = self.fetch_wallet(wallet_metadata.clone()).await?;
let old_wallet = Wallet::try_from(api_wallet).map_err(FundsManagerError::custom)?;
let root_key =
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?;

// Send a withdrawal request to the relayer
let req = Self::build_withdrawal_request(&mint, &deposit_address, &old_wallet)?;
self.relayer_client.withdraw_balance(wallet_metadata.id, mint, req, root_key).await?;

Ok(())
}

// -----------
// | Helpers |
// -----------

/// Fetch a wallet given its metadata
///
/// This is done by:
Expand All @@ -38,8 +87,86 @@ impl Indexer {
derive_wallet_keychain(&eth_key, self.chain_id).map_err(FundsManagerError::custom)?;
let root_key = wallet_keychain.secret_keys.sk_root.clone().expect("root key not present");

// Fetch the wallet from the relayer
let wallet = self.relayer_client.get_wallet(wallet_metadata.id, &root_key).await?;
Ok(wallet.wallet)
// Fetch the wallet from the relayer and replace the keychain so that we have
// access to the full set of secret keys
let mut wallet =
self.relayer_client.get_wallet(wallet_metadata.id, &root_key).await?.wallet;
wallet.key_chain = wallet_keychain.into();

Ok(wallet)
}

/// Build a withdrawal request
fn build_withdrawal_request(
mint: &str,
to: &str,
old_wallet: &Wallet,
) -> Result<WithdrawBalanceRequest, FundsManagerError> {
// Withdraw the balance from the wallet
let mut new_wallet = old_wallet.clone();
let mint_bigint = biguint_from_hex_string(mint).map_err(FundsManagerError::custom)?;
let bal = new_wallet.get_balance(&mint_bigint).cloned().ok_or_else(|| {
FundsManagerError::custom(format!("No balance found for mint {mint}"))
})?;

if bal.amount == 0 {
return Err(FundsManagerError::custom(format!("Balance for mint {mint} is 0")));
}
new_wallet.withdraw(&mint_bigint, bal.amount).map_err(FundsManagerError::custom)?;
new_wallet.reblind_wallet();

// Sign the commitment to the new wallet and the transfer to the deposit address
let root_key =
old_wallet.key_chain.secret_keys.sk_root.as_ref().expect("root key not present");
let commitment_sig = old_wallet
.sign_commitment(new_wallet.get_wallet_share_commitment())
.expect("failed to sign wallet commitment");

let dest_bigint = biguint_from_hex_string(to).map_err(FundsManagerError::custom)?;
let transfer_sig = Self::authorize_withdrawal(
root_key,
mint_bigint.clone(),
bal.amount,
dest_bigint.clone(),
)?;

Ok(WithdrawBalanceRequest {
destination_addr: dest_bigint,
amount: BigUint::from(bal.amount),
wallet_commitment_sig: commitment_sig.to_vec(),
external_transfer_sig: transfer_sig.to_vec(),
})
}

/// Authorize a withdrawal from the darkpool
fn authorize_withdrawal(
root_key: &SecretSigningKey,
mint: BigUint,
amount: Amount,
to: BigUint,
) -> Result<Signature, FundsManagerError> {
let converted_key: SigningKey = root_key.try_into().expect("key conversion failed");

// Construct a transfer
let transfer = ExternalTransfer {
mint,
amount,
direction: ExternalTransferDirection::Withdrawal,
account_addr: to,
};

// Sign the transfer with the root key
let contract_transfer =
to_contract_external_transfer(&transfer).map_err(FundsManagerError::custom)?;
let buf = serialize_calldata(&contract_transfer).map_err(FundsManagerError::custom)?;
let digest = keccak256(&buf);
let (sig, recovery_id) =
converted_key.sign_prehash_recoverable(&digest).map_err(FundsManagerError::custom)?;

Ok(Signature {
r: U256::from_big_endian(&sig.r().to_bytes()),
s: U256::from_big_endian(&sig.s().to_bytes()),
v: recovery_id.to_byte() as u64,
})
}
}
6 changes: 6 additions & 0 deletions funds-manager/funds-manager-server/src/fee_indexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use diesel_async::AsyncPgConnection;
use renegade_circuit_types::elgamal::DecryptionKey;
use renegade_util::hex::jubjub_from_hex_string;

use crate::custody_client::CustodyClient;
use crate::relayer_client::RelayerClient;

pub mod fee_balances;
Expand All @@ -29,10 +30,13 @@ pub(crate) struct Indexer {
pub db_conn: AsyncPgConnection,
/// The AWS config
pub aws_config: AwsConfig,
/// The custody client
pub custody_client: CustodyClient,
}

impl Indexer {
/// Constructor
#[allow(clippy::too_many_arguments)]
pub fn new(
chain_id: u64,
chain: Chain,
Expand All @@ -41,6 +45,7 @@ impl Indexer {
decryption_keys: Vec<DecryptionKey>,
db_conn: AsyncPgConnection,
relayer_client: RelayerClient,
custody_client: CustodyClient,
) -> Self {
Indexer {
chain_id,
Expand All @@ -50,6 +55,7 @@ impl Indexer {
db_conn,
relayer_client,
aws_config,
custody_client,
}
}

Expand Down
15 changes: 14 additions & 1 deletion funds-manager/funds-manager-server/src/fee_indexer/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use diesel_async::RunQueryDsl;
use renegade_common::types::wallet::WalletIdentifier;
use renegade_constants::MAX_BALANCES;
use tracing::warn;
use uuid::Uuid;

use crate::db::models::WalletMetadata;
use crate::db::models::{Metadata, NewFee};
Expand All @@ -26,7 +27,7 @@ use crate::db::schema::{
indexing_metadata::dsl::{
indexing_metadata as metadata_table, key as metadata_key, value as metadata_value,
},
wallets::dsl::{mints as managed_mints_col, wallets as wallet_table},
wallets::dsl::{id as wallet_id_col, mints as managed_mints_col, wallets as wallet_table},
};
use crate::error::FundsManagerError;
use crate::Indexer;
Expand Down Expand Up @@ -208,6 +209,18 @@ impl Indexer {
// | Wallets Table |
// -----------------

/// Get a wallet by its ID
pub(crate) async fn get_wallet_by_id(
&mut self,
wallet_id: &Uuid,
) -> Result<WalletMetadata, FundsManagerError> {
wallet_table
.filter(wallet_id_col.eq(wallet_id))
.first::<WalletMetadata>(&mut self.db_conn)
.await
.map_err(|e| FundsManagerError::db(format!("failed to get wallet by ID: {}", e)))
}

/// Get all wallets in the table
pub(crate) async fn get_all_wallets(
&mut self,
Expand Down
17 changes: 16 additions & 1 deletion funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::error::ApiError;
use crate::Server;
use bytes::Bytes;
use funds_manager_api::{
DepositAddressResponse, FeeWalletsResponse, WithdrawFundsRequest, WithdrawGasRequest,
DepositAddressResponse, FeeWalletsResponse, WithdrawFeeBalanceRequest, WithdrawFundsRequest,
WithdrawGasRequest,
};
use std::collections::HashMap;
use std::sync::Arc;
Expand Down Expand Up @@ -116,3 +117,17 @@ pub(crate) async fn get_fee_wallets_handler(
let wallets = indexer.fetch_fee_wallets().await?;
Ok(warp::reply::json(&FeeWalletsResponse { wallets }))
}

/// Handler for withdrawing a fee balance
pub(crate) async fn withdraw_fee_balance_handler(
req: WithdrawFeeBalanceRequest,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
let mut indexer = server.build_indexer().await?;
indexer
.withdraw_fee_balance(req.wallet_id, req.mint)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;

Ok(warp::reply::json(&"Fee withdrawal initiated..."))
}
19 changes: 16 additions & 3 deletions funds-manager/funds-manager-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ use error::FundsManagerError;
use ethers::signers::LocalWallet;
use fee_indexer::Indexer;
use funds_manager_api::{
WithdrawGasRequest, GET_DEPOSIT_ADDRESS_ROUTE, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE,
PING_ROUTE, REDEEM_FEES_ROUTE, WITHDRAW_CUSTODY_ROUTE, WITHDRAW_GAS_ROUTE,
WithdrawFeeBalanceRequest, WithdrawGasRequest, GET_DEPOSIT_ADDRESS_ROUTE,
GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, PING_ROUTE, REDEEM_FEES_ROUTE, WITHDRAW_CUSTODY_ROUTE,
WITHDRAW_FEE_BALANCE_ROUTE, WITHDRAW_GAS_ROUTE,
};
use handlers::{
get_deposit_address_handler, get_fee_wallets_handler, index_fees_handler,
quoter_withdraw_handler, redeem_fees_handler, withdraw_gas_handler,
quoter_withdraw_handler, redeem_fees_handler, withdraw_fee_balance_handler,
withdraw_gas_handler,
};
use middleware::{identity, with_hmac_auth, with_json_body};
use relayer_client::RelayerClient;
Expand Down Expand Up @@ -193,6 +195,7 @@ impl Server {
self.decryption_keys.clone(),
db_conn,
self.relayer_client.clone(),
self.custody_client.clone(),
))
}
}
Expand Down Expand Up @@ -306,13 +309,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
.and(with_server(server.clone()))
.and_then(get_fee_wallets_handler);

let withdraw_fee_balance = warp::post()
.and(warp::path("fees"))
.and(warp::path(WITHDRAW_FEE_BALANCE_ROUTE))
.and(with_hmac_auth(server.clone()))
.map(with_json_body::<WithdrawFeeBalanceRequest>)
.and_then(identity)
.and(with_server(server.clone()))
.and_then(withdraw_fee_balance_handler);

let routes = ping
.or(index_fees)
.or(redeem_fees)
.or(withdraw_custody)
.or(get_deposit_address)
.or(withdraw_gas)
.or(get_balances)
.or(withdraw_fee_balance)
.recover(handle_rejection);
warp::serve(routes).run(([0, 0, 0, 0], cli.port)).await;

Expand Down
Loading

0 comments on commit e30f86c

Please sign in to comment.