Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

funds-manager: fee-indexer: Allow withdrawal of fees to custody #16

Merged
merged 1 commit into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading