diff --git a/funds-manager/funds-manager-api/src/types/quoters.rs b/funds-manager/funds-manager-api/src/types/quoters.rs index 3d8e61d..5c1bef2 100644 --- a/funds-manager/funds-manager-api/src/types/quoters.rs +++ b/funds-manager/funds-manager-api/src/types/quoters.rs @@ -44,7 +44,7 @@ pub struct WithdrawFundsRequest { // --- Execution --- // /// The subset of the quote response forwarded to consumers of this client -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecutionQuote { /// The token address we're buying @@ -79,9 +79,11 @@ pub struct ExecutionQuote { #[derive(Debug, Serialize, Deserialize)] pub struct GetExecutionQuoteRequest { /// The token address we're buying - pub buy_token_address: String, + #[serde(with = "address_string_serialization")] + pub buy_token_address: Address, /// The token address we're selling - pub sell_token_address: String, + #[serde(with = "address_string_serialization")] + pub sell_token_address: Address, /// The amount of tokens to sell pub sell_amount: u128, } 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 cac5096..6945278 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 @@ -138,7 +138,9 @@ impl CustodyClient { let secret_name = Self::hot_wallet_secret_name(address); let secret_value = get_secret(&secret_name, &self.aws_config).await?; - LocalWallet::from_str(&secret_value).map_err(FundsManagerError::parse) + LocalWallet::from_str(&secret_value) + .map_err(FundsManagerError::parse) + .map(|w| w.with_chain_id(self.chain_id)) } /// Fetch the token balance at the given address for a wallet 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 7e7876d..975bcd8 100644 --- a/funds-manager/funds-manager-server/src/execution_client/mod.rs +++ b/funds-manager/funds-manager-server/src/execution_client/mod.rs @@ -79,10 +79,9 @@ impl ExecutionClient { let status = response.status(); if status != StatusCode::OK { let body = response.text().await?; - error!("Unexpected status code: {status}\nbody: {body}"); - return Err(ExecutionClientError::http(format!( - "Unexpected status code: {status}\nbody: {body}" - ))); + let msg = format!("Unexpected status code: {status}\nbody: {body}"); + error!(msg); + return Err(ExecutionClientError::http(msg)); } response.json::().await.map_err(ExecutionClientError::http) 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 550062a..c810580 100644 --- a/funds-manager/funds-manager-server/src/execution_client/quotes.rs +++ b/funds-manager/funds-manager-server/src/execution_client/quotes.rs @@ -1,7 +1,16 @@ //! Client methods for fetching quotes and prices from the execution venue +use std::{str::FromStr, sync::Arc}; + +use ethers::{ + signers::{LocalWallet, Signer}, + types::{Address, U256}, +}; use funds_manager_api::quoters::ExecutionQuote; use serde::Deserialize; +use tracing::info; + +use crate::helpers::ERC20; use super::{error::ExecutionClientError, ExecutionClient}; @@ -19,6 +28,14 @@ const SELL_AMOUNT: &str = "sellAmount"; /// The taker address url param const TAKER_ADDRESS: &str = "takerAddress"; +/// The 0x exchange proxy contract address +/// +/// TODO: This is the same across _most_ chains, but if we wish to support +/// one-off chains like ethereum sepolia, we should make this configurable +/// +/// See: https://0x.org/docs/introduction/0x-cheat-sheet#exchange-proxy-addresses +const EXCHANGE_PROXY_ADDRESS: &str = "0xdef1c0ded9bec7f1a1670819833240f027b25eff"; + /// The price response #[derive(Debug, Deserialize)] pub struct PriceResponse { @@ -45,19 +62,60 @@ impl ExecutionClient { /// Fetch a quote for an asset pub async fn get_quote( &self, - buy_asset: &str, - sell_asset: &str, + buy_asset: Address, + sell_asset: Address, amount: u128, - recipient: &str, + wallet: &LocalWallet, ) -> Result { + // First, set an approval for the sell token, the 0x api will not give a quote + // if its contract is not an approved spender for the requested amount + let exchange_addr = Address::from_str(EXCHANGE_PROXY_ADDRESS).unwrap(); + self.approve_erc20_allowance(sell_asset, exchange_addr, U256::from(amount), wallet).await?; + + let buy = format!("{buy_asset:#x}"); + let sell = format!("{sell_asset:#x}"); + let recipient = format!("{:#x}", wallet.address()); let amount_str = amount.to_string(); let params = [ - (BUY_TOKEN, buy_asset), - (SELL_TOKEN, sell_asset), + (BUY_TOKEN, buy.as_str()), + (SELL_TOKEN, sell.as_str()), (SELL_AMOUNT, amount_str.as_str()), - (TAKER_ADDRESS, recipient), + (TAKER_ADDRESS, recipient.as_str()), ]; self.send_get_request(QUOTE_ENDPOINT, ¶ms).await } + + /// Approve an erc20 allowance + async fn approve_erc20_allowance( + &self, + token_address: Address, + spender: Address, + amount: U256, + wallet: &LocalWallet, + ) -> Result<(), ExecutionClientError> { + let client = self.get_signer(wallet.clone()); + let erc20 = ERC20::new(token_address, Arc::new(client)); + + // First, check if the allowance is already sufficient + let allowance = erc20 + .allowance(wallet.address(), spender) + .await + .map_err(ExecutionClientError::arbitrum)?; + if allowance >= amount { + info!("Already approved erc20 allowance for {spender:#x}"); + return Ok(()); + } + + // Otherwise, approve the allowance + let tx = erc20.approve(spender, amount); + let pending_tx = tx.send().await.map_err(ExecutionClientError::arbitrum)?; + + let receipt = pending_tx + .await + .map_err(ExecutionClientError::arbitrum)? + .ok_or_else(|| ExecutionClientError::arbitrum("Transaction failed"))?; + info!("Approved erc20 allowance at: {:#x}", receipt.transaction_hash); + Ok(()) + } } diff --git a/funds-manager/funds-manager-server/src/execution_client/swap.rs b/funds-manager/funds-manager-server/src/execution_client/swap.rs index 5a74047..4250682 100644 --- a/funds-manager/funds-manager-server/src/execution_client/swap.rs +++ b/funds-manager/funds-manager-server/src/execution_client/swap.rs @@ -1,16 +1,14 @@ //! Handlers for executing swaps -use std::sync::Arc; - use ethers::{ providers::Middleware, signers::LocalWallet, - types::{Address, Eip1559TransactionRequest, TransactionReceipt, U256}, + types::{Eip1559TransactionRequest, TransactionReceipt}, }; use funds_manager_api::quoters::ExecutionQuote; use tracing::info; -use crate::helpers::{TransactionHash, ERC20}; +use crate::helpers::TransactionHash; use super::{error::ExecutionClientError, ExecutionClient}; @@ -19,48 +17,20 @@ impl ExecutionClient { pub async fn execute_swap( &self, quote: ExecutionQuote, - wallet: LocalWallet, + wallet: &LocalWallet, ) -> Result { - // Approve the necessary ERC20 allowance - self.approve_erc20_allowance( - quote.sell_token_address, - quote.to, - quote.sell_amount, - &wallet, - ) - .await?; - // Execute the swap let receipt = self.execute_swap_tx(quote, wallet).await?; let tx_hash = receipt.transaction_hash; - info!("Swap executed at {tx_hash}"); + info!("Swap executed at {tx_hash:#x}"); Ok(tx_hash) } - /// 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, + wallet: &LocalWallet, ) -> Result { let client = self.get_signer(wallet.clone()); let tx = Eip1559TransactionRequest::new() diff --git a/funds-manager/funds-manager-server/src/handlers.rs b/funds-manager/funds-manager-server/src/handlers.rs index 51b767a..ad5ae1d 100644 --- a/funds-manager/funds-manager-server/src/handlers.rs +++ b/funds-manager/funds-manager-server/src/handlers.rs @@ -124,10 +124,10 @@ pub(crate) async fn get_execution_quote_handler( // Fetch the quoter hot wallet information let vault = DepositWithdrawSource::Quoter.vault_name(); let hot_wallet = server.custody_client.get_hot_wallet_by_vault(vault).await?; - let recipient = format!("0x{}", hot_wallet.address); + let wallet = server.custody_client.get_hot_wallet_private_key(&hot_wallet.address).await?; let quote = server .execution_client - .get_quote(&req.buy_token_address, &req.sell_token_address, req.sell_amount, &recipient) + .get_quote(req.buy_token_address, req.sell_token_address, req.sell_amount, &wallet) .await .map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?; @@ -144,8 +144,8 @@ pub(crate) async fn execute_swap_handler( let hot_wallet = server.custody_client.get_hot_wallet_by_vault(vault).await?; let wallet = server.custody_client.get_hot_wallet_private_key(&hot_wallet.address).await?; - let tx = server.execution_client.execute_swap(req.quote, wallet).await?; - let resp = ExecuteSwapResponse { tx_hash: tx.to_string() }; + let tx = server.execution_client.execute_swap(req.quote, &wallet).await?; + let resp = ExecuteSwapResponse { tx_hash: format!("{tx:#x}") }; Ok(warp::reply::json(&resp)) } diff --git a/funds-manager/funds-manager-server/src/helpers.rs b/funds-manager/funds-manager-server/src/helpers.rs index b1e5283..41d5ff7 100644 --- a/funds-manager/funds-manager-server/src/helpers.rs +++ b/funds-manager/funds-manager-server/src/helpers.rs @@ -19,9 +19,10 @@ pub type TransactionHash = H256; 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 balanceOf(address account) external view returns (uint256) + function allowance(address owner, address spender) external view returns (uint256) function approve(address spender, uint256 value) external returns (bool) function transfer(address recipient, uint256 amount) external returns (bool) ]"#