Skip to content

Commit

Permalink
Fees for substrate (#375)
Browse files Browse the repository at this point in the history
Co-authored-by: shekohex <dev+github@shadykhalifa.me>
Co-authored-by: Salman Pathan <pathansalman555@gmail.com>
Co-authored-by: drewstone <drewstone329@gmail.com>
  • Loading branch information
4 people authored May 12, 2023
1 parent 4908f16 commit e939c7b
Show file tree
Hide file tree
Showing 20 changed files with 645 additions and 258 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,18 +488,20 @@ cargo test

### To run E2E tests

First you will need [`protocol-substrate`](https://github.com/webb-tools/protocol-substrate) node, compiled locally (in release mode) and both the `protocol-substrate` and `relayer` project must be next to each other. The relayer must be compiled using `--features integration-tests,cli`.
First you will need [`protocol-substrate`](https://github.com/webb-tools/protocol-substrate) and [`tangle`](https://github.com/webb-tools/tangle) nodes, compiled locally (in release mode) and both the `protocol-substrate` and `relayer` project must be next to each other. The relayer must be compiled using `--features integration-tests,cli`.

Here is the basic setup you will need:

1. Clone the Relayer repo `git clone https://github.com/webb-tools/relayer.git`
2. Clone Protocol Substrate node `https://github.com/webb-tools/protocol-substrate.git`
3. Then fetch the submodules for the node `cd protocol-substrate && git submodule update --init`
4. While you are there, build the standalone node `cargo build --release -p webb-standalone-node`
5. And then go back to the relayer `cd ../relayer`
6. Run `cd tests && dvc pull`
7. Run `yarn install` (in `tests` dir)
8. `yarn test`
5. Clone Tangle repo `https://github.com/webb-tools/tangle.git`
6. Build tangle `cd tangle && cargo build --release --features integration-tests -p tangle-standalone`
7. And then go back to the relayer `cd ../relayer`
8. Run `cd tests && dvc pull`
9. Run `yarn install` (in `tests` dir)
10. `yarn test`

### Tips for E2E tests

Expand Down
24 changes: 21 additions & 3 deletions crates/relayer-handler-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use serde::{Deserialize, Deserializer, Serialize};
use tokio::sync::mpsc;
use webb::evm::ethers::abi::Address;
use webb::evm::ethers::prelude::{ContractError, I256};
use webb::evm::ethers::prelude::{ContractError, I256, U128};
use webb::evm::ethers::providers::Middleware;
use webb::evm::ethers::types::Bytes;
use webb::evm::ethers::types::{H256, U256};
Expand Down Expand Up @@ -52,6 +52,24 @@ impl<'de> Deserialize<'de> for WebbI256 {
Ok(WebbI256(i128_val))
}
}
/// A wrapper type around [`i128`] that implements a correct way for [`Serialize`] and [`Deserialize`].
///
/// This supports the signed integer hex values that are not originally supported by the [`i128`] type.
#[derive(Debug, Clone, Serialize)]
#[serde(transparent)]
pub struct WebbI128(pub i128);

impl<'de> Deserialize<'de> for WebbI128 {
fn deserialize<D>(deserializer: D) -> Result<WebbI128, D::Error>
where
D: Deserializer<'de>,
{
let i128_str = String::deserialize(deserializer)?;
let value = i128::from_str_radix(&i128_str, 16)
.map_err(serde::de::Error::custom)?;
Ok(WebbI128(value))
}
}

/// Type of Command to use
#[derive(Debug, Clone, Deserialize)]
Expand Down Expand Up @@ -170,8 +188,8 @@ type P = Vec<u8>; // Substrate raw proof bytes
type R = Vec<[u8; 32]>; // Substrate roots format
type E = [u8; 32]; // Substrate element type
type I = AccountId32; // Substrate account identifier
type B = u128; // Substrate balance type
type A = i128; // Substrate signed amount type
type B = U128; // Substrate balance type
type A = WebbI128; // Substrate signed amount type
type T = u32; // Substrate assetId

/// The command type for Substrate mixer txes
Expand Down
34 changes: 29 additions & 5 deletions crates/relayer-handlers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use std::sync::Arc;
use futures::prelude::*;

use axum::extract::ws::{Message, WebSocket};
use axum::http::StatusCode;
use axum::response::Response;
use axum::Json;
use axum_client_ip::InsecureClientIp;
Expand All @@ -36,9 +37,12 @@ use webb_relayer_handler_utils::{
Command, CommandResponse, CommandStream, EvmCommandType,
IpInformationResponse, SubstrateCommandType,
};
use webb_relayer_tx_relay::evm::fees::{get_fee_info, FeeInfo};
use webb_relayer_tx_relay::evm::fees::{get_evm_fee_info, EvmFeeInfo};

use webb_relayer_tx_relay::evm::vanchor::handle_vanchor_relay_tx;
use webb_relayer_tx_relay::substrate::fees::{
get_substrate_fee_info, SubstrateFeeInfo,
};
use webb_relayer_tx_relay::substrate::mixer::handle_substrate_mixer_relay_tx;
use webb_relayer_tx_relay::substrate::vanchor::handle_substrate_vanchor_relay_tx;
use webb_relayer_utils::HandlerError;
Expand Down Expand Up @@ -201,13 +205,33 @@ pub async fn handle_cmd(
/// * `vanchor` - Address of the smart contract
/// * `gas_amount` - How much gas the transaction needs. Don't use U256 here because it
/// gets parsed incorrectly.
pub async fn handle_fee_info(
pub async fn handle_evm_fee_info(
State(ctx): State<Arc<RelayerContext>>,
Path((chain_id, vanchor, gas_amount)): Path<(u64, Address, u64)>,
) -> Result<Json<FeeInfo>, HandlerError> {
) -> Result<Json<EvmFeeInfo>, HandlerError> {
let chain_id = TypedChainId::from(chain_id);
let gas_amount = U256::from(gas_amount);
Ok(get_fee_info(chain_id, vanchor, gas_amount, ctx.as_ref())
Ok(
get_evm_fee_info(chain_id, vanchor, gas_amount, ctx.as_ref())
.await
.map(Json)?,
)
}

/// Handler for fee estimation
///
/// # Arguments
/// * `chain_id` - ID of the blockchain
/// * `estimated_tx_fees` - Estimated transaction fees
/// * `ctx` - RelayContext reference that holds the configuration
pub async fn handle_substrate_fee_info(
State(ctx): State<Arc<RelayerContext>>,
Path((chain_id, estimated_tx_fees)): Path<(u64, u128)>,
) -> Result<Json<SubstrateFeeInfo>, HandlerError> {
get_substrate_fee_info(chain_id, estimated_tx_fees.into(), ctx.as_ref())
.await
.map(Json)?)
.map(Json)
.map_err(|e| {
HandlerError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})
}
1 change: 1 addition & 0 deletions crates/tx-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ native-tls = { workspace = true, optional = true }
webb-proposals = { workspace = true }
ethereum-types = { workspace = true }
serde = { workspace = true }
sp-core = { workspace = true }

once_cell = "1.17.0"
chrono = { version = "0.4.23", features = ["serde"] }
Expand Down
71 changes: 51 additions & 20 deletions crates/tx-relay/src/evm/fees.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{MAX_REFUND_USD, TRANSACTION_PROFIT_USD};
use chrono::DateTime;
use chrono::Duration;
use chrono::Utc;
use once_cell::sync::Lazy;
use serde::Serialize;
use std::cmp::min;
use std::collections::HashMap;
use std::ops::Add;
use std::sync::{Arc, Mutex};
Expand All @@ -11,6 +13,8 @@ use webb::evm::contract::protocol_solidity::{
};
use webb::evm::ethers::middleware::gas_oracle::GasOracle;
use webb::evm::ethers::prelude::U256;
use webb::evm::ethers::providers::Middleware;
use webb::evm::ethers::signers::Signer;
use webb::evm::ethers::types::Address;
use webb::evm::ethers::utils::{format_units, parse_units};
use webb_chains_info::chain_info_by_chain_id;
Expand All @@ -19,22 +23,19 @@ use webb_proposals::TypedChainId;
use webb_relayer_context::RelayerContext;
use webb_relayer_utils::Result;

/// Maximum refund amount per relay transaction in USD.
const MAX_REFUND_USD: f64 = 5.;
/// Amount of time for which a `FeeInfo` is valid after creation
static FEE_CACHE_TIME: Lazy<Duration> = Lazy::new(|| Duration::minutes(1));
/// Amount of profit that the relay should make with each transaction (in USD).
const TRANSACTION_PROFIT_USD: f64 = 5.;

/// Cache for previously generated fee info. Key consists of the VAnchor address and chain id.
/// Entries are valid as long as `timestamp` is no older than `FEE_CACHE_TIME`.
static FEE_INFO_CACHED: Lazy<Mutex<HashMap<(Address, TypedChainId), FeeInfo>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
static FEE_INFO_CACHED: Lazy<
Mutex<HashMap<(Address, TypedChainId), EvmFeeInfo>>,
> = Lazy::new(|| Mutex::new(HashMap::new()));

/// Return value of fee_info API call. Contains information about relay transaction fee and refunds.
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FeeInfo {
pub struct EvmFeeInfo {
/// Estimated fee for an average relay transaction, in `wrappedToken`. This is only for
/// display to the user
pub estimated_fee: U256,
Expand All @@ -49,6 +50,9 @@ pub struct FeeInfo {
/// Price of the native token in USD, internally cached to recalculate estimated fee
#[serde(skip)]
native_token_price: f64,
/// Number of decimals of the native token, internally cached to recalculate max refund
#[serde(skip)]
native_token_decimals: u8,
/// Price of the wrapped token in USD, internally cached to recalculate estimated fee
#[serde(skip)]
wrapped_token_price: f64,
Expand All @@ -61,12 +65,12 @@ pub struct FeeInfo {
///
/// If fee info was recently requested, the cached value is used. Otherwise it is regenerated
/// based on the current exchange rate and estimated gas price.
pub async fn get_fee_info(
pub async fn get_evm_fee_info(
chain_id: TypedChainId,
vanchor: Address,
gas_amount: U256,
ctx: &RelayerContext,
) -> Result<FeeInfo> {
) -> Result<EvmFeeInfo> {
// Retrieve cached fee info item
let fee_info_cached = {
let mut lock =
Expand All @@ -89,6 +93,14 @@ pub async fn get_fee_info(
fee_info.wrapped_token_price,
fee_info.wrapped_token_decimals,
)?;
// Recalculate max refund in case relayer balance changed.
fee_info.max_refund = max_refund(
chain_id,
fee_info.native_token_price,
fee_info.native_token_decimals,
ctx,
)
.await?;
Ok(fee_info)
} else {
let fee_info =
Expand All @@ -109,7 +121,7 @@ async fn generate_fee_info(
vanchor: Address,
gas_amount: U256,
ctx: &RelayerContext,
) -> Result<FeeInfo> {
) -> Result<EvmFeeInfo> {
// Get token names
let (native_token, native_token_decimals) =
get_native_token_name_and_decimals(chain_id)?;
Expand Down Expand Up @@ -162,25 +174,44 @@ async fn generate_fee_info(
)?
.into();

// Calculate the maximum refund amount per relay transaction in `nativeToken`.
let max_refund = parse_units(
MAX_REFUND_USD / native_token_price,
u32::from(native_token_decimals),
)?
.into();

Ok(FeeInfo {
Ok(EvmFeeInfo {
estimated_fee,
gas_price,
refund_exchange_rate,
max_refund,
max_refund: max_refund(
chain_id,
native_token_price,
native_token_decimals,
ctx,
)
.await?,
timestamp: Utc::now(),
native_token_price,
native_token_decimals,
wrapped_token_price,
wrapped_token_decimals,
})
}

async fn max_refund(
chain_id: TypedChainId,
native_token_price: f64,
native_token_decimals: u8,
ctx: &RelayerContext,
) -> Result<U256> {
let wallet = ctx.evm_wallet(chain_id.underlying_chain_id()).await?;
let provider = ctx.evm_provider(chain_id.underlying_chain_id()).await?;
let relayer_balance = provider.get_balance(wallet.address(), None).await?;
// Calculate the maximum refund amount per relay transaction in `nativeToken`.
// Ensuring that refund <= relayer balance
let max_refund = parse_units(
MAX_REFUND_USD / native_token_price,
u32::from(native_token_decimals),
)?
.into();
Ok(min(relayer_balance, max_refund))
}

/// Pull USD prices of base token from coingecko.com, and use this to calculate the transaction
/// fee in `wrappedToken` wei. This fee includes a profit for the relay of `TRANSACTION_PROFIT_USD`.
///
Expand Down Expand Up @@ -265,7 +296,7 @@ fn get_native_token_name_and_decimals(
},
),
Substrate(id) => match id {
1080 => Ok(("tTNT", 18)),
1081 => Ok(("tTNT", 18)),
_ => {
// During testing, we will use the tTNT token for all substrate chains.
if cfg!(debug_assertions) {
Expand Down
29 changes: 12 additions & 17 deletions crates/tx-relay/src/evm/vanchor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use super::*;
use crate::evm::fees::{get_fee_info, FeeInfo};
use crate::evm::fees::{get_evm_fee_info, EvmFeeInfo};
use crate::evm::handle_evm_tx;
use ethereum_types::U256;
use futures::TryFutureExt;
use std::{collections::HashMap, sync::Arc};
use webb::evm::ethers::utils::{format_units, parse_ether};
use webb::evm::{
Expand Down Expand Up @@ -81,19 +82,6 @@ pub async fn handle_vanchor_relay_tx<'a>(
})?;
let _ = stream.send(Network(NetworkStatus::Connected)).await;

// ensure that relayer has enough balance for refund
let relayer_balance = provider
.get_balance(wallet.address(), None)
.await
.map_err(|e| {
Error(format!("Failed to retrieve relayer balance: {e}"))
})?;
if cmd.ext_data.refund > relayer_balance {
return Err(Error(
"Requested refund is higher than relayer balance".to_string(),
));
}

let client = Arc::new(SignerMiddleware::new(provider, wallet));
let contract = VAnchorContract::new(cmd.id, client.clone());

Expand Down Expand Up @@ -142,6 +130,7 @@ pub async fn handle_vanchor_relay_tx<'a>(
public_inputs,
encryptions,
);

if !cmd.ext_data.refund.is_zero() {
call = call.value(cmd.ext_data.refund);
}
Expand All @@ -153,7 +142,7 @@ pub async fn handle_vanchor_relay_tx<'a>(
})
})?;
let typed_chain_id = TypedChainId::Evm(chain.chain_id);
let fee_info = get_fee_info(
let fee_info = get_evm_fee_info(
typed_chain_id,
contract_config.common.address,
gas_amount,
Expand Down Expand Up @@ -186,7 +175,8 @@ pub async fn handle_vanchor_relay_tx<'a>(
if cmd.ext_data.fee < adjusted_fee + wrapped_amount {
let msg = format!(
"User sent a fee that is too low {} but expected {}",
cmd.ext_data.fee, fee_info.estimated_fee
cmd.ext_data.fee,
adjusted_fee + wrapped_amount
);
return Err(Error(msg));
}
Expand Down Expand Up @@ -214,6 +204,11 @@ pub async fn handle_vanchor_relay_tx<'a>(
.total_fee_earned
.inc_by(cmd.ext_data.fee.as_u128() as f64);

let relayer_balance = client
.get_balance(client.signer().address(), None)
.unwrap_or_else(|_| U256::zero())
.await;

metrics
.account_balance_entry(typed_chain_id)
.set(wei_to_gwei(relayer_balance));
Expand All @@ -222,7 +217,7 @@ pub async fn handle_vanchor_relay_tx<'a>(

fn calculate_wrapped_refund_amount(
refund: U256,
fee_info: &FeeInfo,
fee_info: &EvmFeeInfo,
) -> webb_relayer_utils::Result<U256> {
let refund_exchange_rate: f32 =
format_units(fee_info.refund_exchange_rate, "ether")?.parse()?;
Expand Down
5 changes: 5 additions & 0 deletions crates/tx-relay/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ pub mod evm;
/// Substrate Transactional Relayer.
#[cfg(feature = "substrate")]
pub mod substrate;

/// Maximum refund amount per relay transaction in USD.
const MAX_REFUND_USD: f64 = 5.;
/// Amount of profit that the relay should make with each transaction (in USD).
const TRANSACTION_PROFIT_USD: f64 = 5.;
Loading

0 comments on commit e939c7b

Please sign in to comment.