Skip to content

Commit

Permalink
funds-manager: execution-client: Approve ERC20 when requesting quote
Browse files Browse the repository at this point in the history
The 0x swap API needs the tokens pre-approved. So it is not sufficient
to approve them when the swap happens
  • Loading branch information
joeykraut committed Aug 10, 2024
1 parent 46d592d commit 445290e
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 54 deletions.
8 changes: 5 additions & 3 deletions funds-manager/funds-manager-api/src/types/quoters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::<T>().await.map_err(ExecutionClientError::http)
Expand Down
70 changes: 64 additions & 6 deletions funds-manager/funds-manager-server/src/execution_client/quotes.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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 {
Expand All @@ -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<ExecutionQuote, ExecutionClientError> {
// 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, &params).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(())
}
}
40 changes: 5 additions & 35 deletions funds-manager/funds-manager-server/src/execution_client/swap.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -19,48 +17,20 @@ impl ExecutionClient {
pub async fn execute_swap(
&self,
quote: ExecutionQuote,
wallet: LocalWallet,
wallet: &LocalWallet,
) -> Result<TransactionHash, ExecutionClientError> {
// 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<TransactionReceipt, ExecutionClientError> {
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<TransactionReceipt, ExecutionClientError> {
let client = self.get_signer(wallet.clone());
let tx = Eip1559TransactionRequest::new()
Expand Down
8 changes: 4 additions & 4 deletions funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())))?;

Expand All @@ -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))
}

Expand Down
3 changes: 2 additions & 1 deletion funds-manager/funds-manager-server/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]"#
Expand Down

0 comments on commit 445290e

Please sign in to comment.