diff --git a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs index 436bdc7..ba9158e 100644 --- a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs +++ b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs @@ -16,11 +16,11 @@ use rand::thread_rng; use tracing::info; use uuid::Uuid; -use super::{CustodyClient, ERC20}; +use super::CustodyClient; use crate::{ custody_client::DepositWithdrawSource, error::FundsManagerError, - helpers::{create_secrets_manager_entry_with_description, get_secret}, + helpers::{create_secrets_manager_entry_with_description, get_secret, ERC20}, }; impl CustodyClient { @@ -109,6 +109,7 @@ impl CustodyClient { Ok(()) } + /// Transfer funds from a vault to a hot wallet pub async fn transfer_from_vault_to_hot_wallet( &self, vault: &str, diff --git a/funds-manager/funds-manager-server/src/custody_client/mod.rs b/funds-manager/funds-manager-server/src/custody_client/mod.rs index f8fc26b..44b792e 100644 --- a/funds-manager/funds-manager-server/src/custody_client/mod.rs +++ b/funds-manager/funds-manager-server/src/custody_client/mod.rs @@ -1,5 +1,4 @@ //! Manages the custody backend for the funds manager -#![allow(missing_docs)] pub mod deposit; pub mod gas_wallets; mod hot_wallets; @@ -8,7 +7,6 @@ pub mod withdraw; use aws_config::SdkConfig as AwsConfig; use ethers::middleware::SignerMiddleware; -use ethers::prelude::abigen; use ethers::providers::{Http, Middleware, Provider}; use ethers::signers::{LocalWallet, Signer}; use ethers::types::{Address, TransactionReceipt, TransactionRequest}; @@ -26,16 +24,7 @@ use tracing::info; use crate::db::{DbConn, DbPool}; use crate::error::FundsManagerError; - -abigen!( - ERC20, - r#"[ - function balanceOf(address account) external view returns (uint256) - function transfer(address recipient, uint256 amount) external returns (bool) - function symbol() external view returns (string memory) - function decimals() external view returns (uint8) - ]"# -); +use crate::helpers::ERC20; /// The source of a deposit #[derive(Clone, Copy)] diff --git a/funds-manager/funds-manager-server/src/custody_client/withdraw.rs b/funds-manager/funds-manager-server/src/custody_client/withdraw.rs index 4eefc7a..8a8de68 100644 --- a/funds-manager/funds-manager-server/src/custody_client/withdraw.rs +++ b/funds-manager/funds-manager-server/src/custody_client/withdraw.rs @@ -1,3 +1,4 @@ +//! Withdrawal methods for custodied funds use std::str::FromStr; use crate::{error::FundsManagerError, helpers::get_secret}; diff --git a/funds-manager/funds-manager-server/src/execution_client/error.rs b/funds-manager/funds-manager-server/src/execution_client/error.rs index 84eb701..47f4f4d 100644 --- a/funds-manager/funds-manager-server/src/execution_client/error.rs +++ b/funds-manager/funds-manager-server/src/execution_client/error.rs @@ -5,6 +5,8 @@ use std::fmt::Display; /// An error returned by the execution client #[derive(Debug, Clone)] pub enum ExecutionClientError { + /// An error interacting with Arbitrum + Arbitrum(String), /// An error returned by the execution client Http(String), /// An error parsing a value @@ -12,6 +14,12 @@ pub enum ExecutionClientError { } impl ExecutionClientError { + /// Create a new arbitrum error + #[allow(clippy::needless_pass_by_value)] + pub fn arbitrum(e: T) -> Self { + ExecutionClientError::Arbitrum(e.to_string()) + } + /// Create a new http error #[allow(clippy::needless_pass_by_value)] pub fn http(e: T) -> Self { @@ -28,6 +36,7 @@ impl ExecutionClientError { impl Display for ExecutionClientError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let msg = match self { + ExecutionClientError::Arbitrum(e) => format!("Arbitrum error: {e}"), ExecutionClientError::Http(e) => format!("HTTP error: {e}"), ExecutionClientError::Parse(e) => format!("Parse error: {e}"), }; diff --git a/funds-manager/funds-manager-server/src/execution_client/mod.rs b/funds-manager/funds-manager-server/src/execution_client/mod.rs index 7f3fde0..0d924a0 100644 --- a/funds-manager/funds-manager-server/src/execution_client/mod.rs +++ b/funds-manager/funds-manager-server/src/execution_client/mod.rs @@ -2,9 +2,15 @@ //! API pub mod error; pub mod quotes; +pub mod swap; use std::sync::Arc; +use ethers::{ + middleware::SignerMiddleware, + providers::{Http, Provider}, + signers::LocalWallet, +}; use reqwest::{Client, Url}; use serde::Deserialize; @@ -22,12 +28,25 @@ pub struct ExecutionClient { base_url: String, /// The underlying HTTP client http_client: Arc, + /// The RPC provider + rpc_provider: Arc>, } impl ExecutionClient { /// Create a new client - pub fn new(api_key: String, base_url: String) -> Self { - Self { api_key, base_url, http_client: Arc::new(Client::new()) } + pub fn new( + api_key: String, + base_url: String, + rpc_url: String, + ) -> Result { + let provider = + Provider::::try_from(&rpc_url).map_err(ExecutionClientError::arbitrum)?; + Ok(Self { + api_key, + base_url, + http_client: Arc::new(Client::new()), + rpc_provider: Arc::new(provider), + }) } /// Get a full URL for a given endpoint @@ -61,4 +80,12 @@ impl ExecutionClient { .await .map_err(ExecutionClientError::http) } + + /// Get an instance of a signer middleware with the http provider attached + fn get_signer( + &self, + wallet: LocalWallet, + ) -> SignerMiddleware>, LocalWallet> { + SignerMiddleware::new(self.rpc_provider.clone(), wallet) + } } diff --git a/funds-manager/funds-manager-server/src/execution_client/quotes.rs b/funds-manager/funds-manager-server/src/execution_client/quotes.rs index 6c42ddb..fe6a297 100644 --- a/funds-manager/funds-manager-server/src/execution_client/quotes.rs +++ b/funds-manager/funds-manager-server/src/execution_client/quotes.rs @@ -1,6 +1,11 @@ //! Client methods for fetching quotes and prices from the execution venue -use serde::Deserialize; +use ethers::types::{Address, Bytes, U256}; +use serde::{Deserialize, Serialize}; + +use crate::helpers::{ + address_string_serialization, bytes_string_serialization, u256_string_serialization, +}; use super::{error::ExecutionClientError, ExecutionClient}; @@ -26,21 +31,34 @@ pub struct PriceResponse { } /// The subset of the quote response forwarded to consumers of this client -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecutionQuote { + /// The token address we're buying + #[serde(with = "address_string_serialization")] + pub buy_token_address: Address, + /// The token address we're selling + #[serde(with = "address_string_serialization")] + pub sell_token_address: Address, + /// The amount of tokens to sell + pub sell_amount: String, /// The quoted price pub price: String, /// The submitting address - pub from: String, + #[serde(with = "address_string_serialization")] + pub from: Address, /// The 0x swap contract address - pub to: String, + #[serde(with = "address_string_serialization")] + pub to: Address, /// The calldata for the swap - pub data: String, + #[serde(with = "bytes_string_serialization")] + pub data: Bytes, /// The value of the tx; should be zero - pub value: String, + #[serde(with = "u256_string_serialization")] + pub value: U256, /// The gas price used in the swap - pub gas_price: String, + #[serde(with = "u256_string_serialization")] + pub gas_price: U256, } impl ExecutionClient { diff --git a/funds-manager/funds-manager-server/src/execution_client/swap.rs b/funds-manager/funds-manager-server/src/execution_client/swap.rs new file mode 100644 index 0000000..55d23fd --- /dev/null +++ b/funds-manager/funds-manager-server/src/execution_client/swap.rs @@ -0,0 +1,76 @@ +//! Handlers for executing swaps + +use std::str::FromStr; +use std::sync::Arc; + +use ethers::{ + providers::Middleware, + signers::LocalWallet, + types::{Address, Eip1559TransactionRequest, TransactionReceipt, U256}, +}; +use tracing::info; + +use crate::helpers::ERC20; + +use super::{error::ExecutionClientError, quotes::ExecutionQuote, ExecutionClient}; + +impl ExecutionClient { + /// Execute a quoted swap + pub async fn execute_swap( + &self, + quote: ExecutionQuote, + wallet: LocalWallet, + ) -> Result<(), ExecutionClientError> { + // Approve the necessary ERC20 allowance + let amount = U256::from_str("e.sell_amount).map_err(ExecutionClientError::parse)?; + self.approve_erc20_allowance(quote.sell_token_address, quote.to, amount, &wallet).await?; + + // Execute the swap + let receipt = self.execute_swap_tx(quote, wallet).await?; + info!("Swap executed at {}", receipt.transaction_hash); + Ok(()) + } + + /// Approve an erc20 allowance + async fn approve_erc20_allowance( + &self, + token_address: Address, + spender: Address, + amount: U256, + wallet: &LocalWallet, + ) -> Result { + let client = self.get_signer(wallet.clone()); + let erc20 = ERC20::new(token_address, Arc::new(client)); + let tx = erc20.approve(spender, amount); + let pending_tx = tx.send().await.map_err(ExecutionClientError::arbitrum)?; + + pending_tx + .await + .map_err(ExecutionClientError::arbitrum)? + .ok_or_else(|| ExecutionClientError::arbitrum("Transaction failed")) + } + + /// Execute a swap + async fn execute_swap_tx( + &self, + quote: ExecutionQuote, + wallet: LocalWallet, + ) -> Result { + let client = self.get_signer(wallet.clone()); + let tx = Eip1559TransactionRequest::new() + .to(quote.to) + .from(quote.from) + .value(quote.value) + .data(quote.data); + + // Send the transaction + let pending_tx = client + .send_transaction(tx, None /* block */) + .await + .map_err(ExecutionClientError::arbitrum)?; + pending_tx + .await + .map_err(ExecutionClientError::arbitrum)? + .ok_or_else(|| ExecutionClientError::arbitrum("Transaction failed")) + } +} diff --git a/funds-manager/funds-manager-server/src/helpers.rs b/funds-manager/funds-manager-server/src/helpers.rs index 9eb4f79..6561993 100644 --- a/funds-manager/funds-manager-server/src/helpers.rs +++ b/funds-manager/funds-manager-server/src/helpers.rs @@ -1,11 +1,33 @@ //! Helpers for the funds manager server +#![allow(missing_docs)] use aws_config::SdkConfig; use aws_sdk_secretsmanager::client::Client as SecretsManagerClient; +use ethers::contract::abigen; use renegade_util::err_str; use crate::error::FundsManagerError; +// --------- +// | ERC20 | +// --------- + +// The ERC20 interface +abigen!( + ERC20, + r#"[ + function balanceOf(address account) external view returns (uint256) + function symbol() external view returns (string memory) + function decimals() external view returns (uint8) + function approve(address spender, uint256 value) external returns (bool) + function transfer(address recipient, uint256 amount) external returns (bool) + ]"# +); + +// ----------------------- +// | AWS Secrets Manager | +// ----------------------- + /// Get a secret from AWS Secrets Manager pub async fn get_secret( secret_name: &str, @@ -57,3 +79,102 @@ pub async fn create_secrets_manager_entry_with_description( Ok(()) } + +// ----------------- +// | Serialization | +// ----------------- + +/// A module for serializing and deserializing addresses as strings +pub(crate) mod address_string_serialization { + use std::str::FromStr; + + use ethers::types::Address; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + /// Serialize an address to a string + pub fn serialize(address: &Address, s: S) -> Result { + s.serialize_str(&address.to_string()) + } + + /// Deserialize a string to an address + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + Address::from_str(&s).map_err(|_| D::Error::custom("Invalid address")) + } +} + +/// A module for serializing and deserializing U256 as strings +pub(crate) mod u256_string_serialization { + use ethers::types::U256; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + /// Serialize a U256 to a string + pub fn serialize(value: &U256, s: S) -> Result { + s.serialize_str(&value.to_string()) + } + + /// Deserialize a string to a U256 + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + U256::from_dec_str(&s).map_err(|_| D::Error::custom("Invalid U256 value")) + } +} + +/// A module for serializing and deserializing bytes from a hex string +pub(crate) mod bytes_string_serialization { + use ethers::types::Bytes; + use hex::FromHex; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + /// Serialize bytes to a hex string + pub fn serialize(value: &Bytes, s: S) -> Result { + let hex = format!("{value:#x}"); + s.serialize_str(&hex) + } + + /// Deserialize a hex string to bytes + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + Bytes::from_hex(s).map_err(|_| D::Error::custom("Invalid bytes value")) + } +} + +#[cfg(test)] +mod tests { + use ethers::types::{Address, Bytes, U256}; + use rand::{thread_rng, Rng}; + + /// Test serialization and deserialization of an address + #[test] + fn test_address_serialization() { + let addr = Address::random(); + let serialized = serde_json::to_string(&addr).unwrap(); + let deserialized: Address = serde_json::from_str(&serialized).unwrap(); + assert_eq!(addr, deserialized); + } + + /// Test serialization and deserialization of a U256 + #[test] + fn test_u256_serialization() { + let mut rng = thread_rng(); + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes); + let value = U256::from(bytes); + + let serialized = serde_json::to_string(&value).unwrap(); + let deserialized: U256 = serde_json::from_str(&serialized).unwrap(); + assert_eq!(value, deserialized); + } + + /// Test serialization and deserialization of bytes + #[test] + fn test_bytes_serialization() { + const N: usize = 32; + let mut rng = thread_rng(); + let bytes: Bytes = (0..N).map(|_| rng.gen_range(0..=u8::MAX)).collect(); + + let serialized = serde_json::to_string(&bytes).unwrap(); + let deserialized: Bytes = serde_json::from_str(&serialized).unwrap(); + assert_eq!(bytes, deserialized); + } +}