Skip to content

Commit

Permalink
auth-server: handle_external_match: mutate calldata for gas sponsorship
Browse files Browse the repository at this point in the history
  • Loading branch information
akirillo committed Jan 30, 2025
1 parent 24b1de5 commit 75c5359
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ target/

.vscode/
/.gitattributes

.env
4 changes: 4 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions auth/auth-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ native-tls = "0.2"
# === Cryptography === #
aes-gcm = "0.10.1"
alloy-sol-types = "=0.7.7"
alloy-primitives = { version = "=0.7.7", features = ["serde", "k256"] }
ethers = "2"
rand = "0.8.5"

Expand Down
18 changes: 18 additions & 0 deletions auth/auth-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ pub enum AuthServerError {
/// An error executing an HTTP request
#[error("Http: {0}")]
Http(String),
/// An error mutating calldata for gas sponsorship
#[error("Calldata mutation error: {0}")]
CalldataMutation(String),
/// An error signing a message
#[error("Signing error: {0}")]
Signing(String),
/// A miscellaneous error
#[error("Error: {0}")]
Custom(String),
Expand Down Expand Up @@ -72,6 +78,18 @@ impl AuthServerError {
pub fn unauthorized<T: ToString>(msg: T) -> Self {
Self::Unauthorized(msg.to_string())
}

/// Create a new calldata mutation error
#[allow(clippy::needless_pass_by_value)]
pub fn calldata_mutation<T: ToString>(msg: T) -> Self {
Self::CalldataMutation(msg.to_string())
}

/// Create a new signing error
#[allow(clippy::needless_pass_by_value)]
pub fn signing<T: ToString>(msg: T) -> Self {
Self::Signing(msg.to_string())
}
}

impl warp::reject::Reject for AuthServerError {}
Expand Down
21 changes: 17 additions & 4 deletions auth/auth-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use tracing::{error, info};
use uuid::Uuid;
use warp::{Filter, Rejection, Reply};

use server::Server;
use server::{handle_external_match::external_quote_assembly_query_params, Server};

/// The default internal server error message
const DEFAULT_INTERNAL_SERVER_ERROR_MESSAGE: &str = "Internal Server Error";
Expand Down Expand Up @@ -99,12 +99,22 @@ pub struct Cli {
#[clap(short, long, env = "RPC_URL")]
rpc_url: String,
/// The address of the darkpool contract
#[clap(short = 'a', long, env = "DARKPOOL_ADDRESS")]
#[clap(short, long, env = "DARKPOOL_ADDRESS")]
darkpool_address: String,
/// The URL of the price reporter
#[arg(long, env = "PRICE_REPORTER_URL")]
pub price_reporter_url: String,

// -------------------
// | Gas Sponsorship |
// -------------------
/// The address of the gas sponsor contract
#[clap(long, env = "GAS_SPONSOR_ADDRESS")]
gas_sponsor_address: String,
/// The auth private key used for gas sponsorship
#[clap(long, env = "GAS_SPONSOR_AUTH_KEY")]
gas_sponsor_auth_key: String,

// -------------
// | Telemetry |
// -------------
Expand Down Expand Up @@ -267,9 +277,12 @@ async fn main() {
.and(warp::path::full())
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and(external_quote_assembly_query_params())
.and(with_server(server.clone()))
.and_then(|path, headers, body, server: Arc<Server>| async move {
server.handle_external_quote_assembly_request(path, headers, body).await
.and_then(|path, headers, body, use_gas_sponsorship, refund_address, server: Arc<Server>| async move {
server
.handle_external_quote_assembly_request(path, headers, body, use_gas_sponsorship, refund_address)
.await
});

let atomic_match_path = warp::path("v0")
Expand Down
194 changes: 191 additions & 3 deletions auth/auth-server/src/server/handle_external_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
//! At a high level the server must first authenticate the request, then forward
//! it to the relayer with admin authentication
use alloy_primitives::Address;
use alloy_sol_types::{sol, SolCall};
use bytes::Bytes;
use http::Method;
use ethers::providers::Middleware;
use ethers::types::transaction::eip2718::TypedTransaction;
use ethers::types::U256;
use http::{Method, Response};
use renegade_arbitrum_client::abi::{
processAtomicMatchSettleCall, processAtomicMatchSettleWithReceiverCall,
};
use serde::{Deserialize, Serialize};
use tracing::{info, instrument, warn};
use warp::{reject::Rejection, reply::Reply};
use warp::{reject::Rejection, reply::Reply, Filter};

use renegade_api::http::external_match::{
AssembleExternalMatchRequest, ExternalMatchRequest, ExternalMatchResponse, ExternalOrder,
Expand All @@ -15,6 +24,7 @@ use renegade_api::http::external_match::{
use renegade_circuit_types::fixed_point::FixedPoint;
use renegade_common::types::{token::Token, TimestampedPrice};

use super::helpers::generate_sponsorship_auth;
use super::Server;
use crate::error::AuthServerError;
use crate::telemetry::helpers::calculate_implied_price;
Expand All @@ -28,6 +38,50 @@ use crate::telemetry::{
},
};

// -------------
// | Constants |
// -------------

/// The gas estimation to use if fetching a gas estimation fails
/// From https://github.com/renegade-fi/renegade/blob/main/workers/api-server/src/http/external_match.rs/#L62
pub const DEFAULT_GAS_ESTIMATION: u64 = 4_000_000; // 4m

// ---------
// | Types |
// ---------

// The ABI for gas sponsorship functions
sol! {
function sponsorAtomicMatchSettle(bytes memory internal_party_match_payload, bytes memory valid_match_settle_atomic_statement, bytes memory match_proofs, bytes memory match_linking_proofs, address memory refund_address, uint256 memory nonce, bytes memory signature) external payable;
function sponsorAtomicMatchSettleWithReceiver(address receiver, bytes memory internal_party_match_payload, bytes memory valid_match_settle_atomic_statement, bytes memory match_proofs, bytes memory match_linking_proofs, address memory refund_address, uint256 memory nonce, bytes memory signature) external payable;
}

/// The query parameters accepted by the external quote assembly endpoint
#[derive(Serialize, Deserialize)]
pub struct ExternalQuoteAssemblyQueryParams {
/// Whether to use gas sponsorship for the assembled quote
pub use_gas_sponsorship: Option<bool>,
/// The address to refund gas to
pub refund_address: Option<Address>,
}

/// A warp filter that extracts the external quote assembly query parameters
pub fn external_quote_assembly_query_params(
) -> impl Filter<Extract = (bool, Address), Error = Rejection> + Clone {
warp::query::<ExternalQuoteAssemblyQueryParams>()
.map(|query_params: ExternalQuoteAssemblyQueryParams| {
(
query_params.use_gas_sponsorship.unwrap_or(false),
query_params.refund_address.unwrap_or(Address::ZERO),
)
})
.untuple_one()
}

// ---------------
// | Server Impl |
// ---------------

/// Handle a proxied request
impl Server {
/// Handle an external quote request
Expand Down Expand Up @@ -64,15 +118,23 @@ impl Server {
path: warp::path::FullPath,
headers: warp::hyper::HeaderMap,
body: Bytes,
use_gas_sponsorship: bool,
refund_address: Address,
) -> Result<impl Reply, Rejection> {
// Authorize the request
let key_desc = self.authorize_request(path.as_str(), &headers, &body).await?;
self.check_bundle_rate_limit(key_desc.clone()).await?;

// Send the request to the relayer
let resp =
let mut resp =
self.send_admin_request(Method::POST, path.as_str(), headers, body.clone()).await?;

// If gas sponsorship is requested, mutate the calldata in the response
// to invoke the gas sponsor contract
if use_gas_sponsorship {
self.mutate_response_for_gas_sponsorship(&mut resp, refund_address).await?;
}

let resp_clone = resp.body().to_vec();
let server_clone = self.clone();
tokio::spawn(async move {
Expand Down Expand Up @@ -330,4 +392,130 @@ impl Server {

Ok(())
}

/// Mutate a quote assembly response for gas sponsorship
async fn mutate_response_for_gas_sponsorship(
&self,
resp: &mut Response<Bytes>,
refund_address: Address,
) -> Result<(), AuthServerError> {
let mut external_match_resp: ExternalMatchResponse =
serde_json::from_slice(resp.body()).map_err(AuthServerError::serde)?;

let gas_sponsor_calldata =
self.generate_gas_sponsor_calldata(&external_match_resp, refund_address)?.into();

external_match_resp.match_bundle.settlement_tx.set_to(self.gas_sponsor_address);
external_match_resp.match_bundle.settlement_tx.set_data(gas_sponsor_calldata);

if external_match_resp.match_bundle.settlement_tx.gas().is_some() {
// If gas was estimated for the match, we need to re-estimate
// - there is extra overhead through the gas sponsor
// contract that must be accounted for.

let gas =
self.estimate_gas(external_match_resp.match_bundle.settlement_tx.clone()).await?;

external_match_resp.match_bundle.settlement_tx.set_gas(gas);
}

let body_mut = resp.body_mut();
*body_mut =
Bytes::from(serde_json::to_vec(&external_match_resp).map_err(AuthServerError::serde)?);

Ok(())
}

/// Generate the calldata for sponsoring the given match via the gas sponsor
fn generate_gas_sponsor_calldata(
&self,
external_match_resp: &ExternalMatchResponse,
refund_address: Address,
) -> Result<Bytes, AuthServerError> {
let calldata = external_match_resp
.match_bundle
.settlement_tx
.data()
.ok_or(AuthServerError::calldata_mutation("expected calldata"))?;

let selector: [u8; 4] = calldata
.as_ref()
.get(0..4)
.ok_or(AuthServerError::calldata_mutation("expected selector"))?
.try_into()
.unwrap();

let gas_sponsor_calldata = match selector {
processAtomicMatchSettleCall::SELECTOR => {
let call = processAtomicMatchSettleCall::abi_decode(
calldata.as_ref(),
true, // validate
)
.map_err(AuthServerError::calldata_mutation)?;

let (nonce, signature) =
generate_sponsorship_auth(refund_address, &self.gas_sponsor_auth_key)?;

let mutated_call = sponsorAtomicMatchSettleCall {
internal_party_match_payload: call.internal_party_match_payload,
valid_match_settle_atomic_statement: call.valid_match_settle_atomic_statement,
match_proofs: call.match_proofs,
match_linking_proofs: call.match_linking_proofs,
refund_address,
nonce,
signature,
};

mutated_call.abi_encode().into()
},
processAtomicMatchSettleWithReceiverCall::SELECTOR => {
let call = processAtomicMatchSettleWithReceiverCall::abi_decode(
calldata.as_ref(),
true, // validate
)
.map_err(AuthServerError::calldata_mutation)?;

let (nonce, signature) =
generate_sponsorship_auth(refund_address, &self.gas_sponsor_auth_key)?;

let mutated_call = sponsorAtomicMatchSettleWithReceiverCall {
receiver: call.receiver,
internal_party_match_payload: call.internal_party_match_payload,
valid_match_settle_atomic_statement: call.valid_match_settle_atomic_statement,
match_proofs: call.match_proofs,
match_linking_proofs: call.match_linking_proofs,
refund_address,
nonce,
signature,
};

mutated_call.abi_encode().into()
},
_ => {
return Err(AuthServerError::calldata_mutation("invalid selector"));
},
};

Ok(gas_sponsor_calldata)
}

/// Estimate the gas for the given external match transaction
async fn estimate_gas(&self, mut tx: TypedTransaction) -> Result<U256, AuthServerError> {
// To estimate gas without reverts, we would need to approve the ERC20 transfers
// due in the transaction before estimating. This is infeasible, so we mock the
// sender as the _darkpool itself_, which will automatically have an approval
// for itself. This can still fail if a transfer exceeds the darkpool's balance,
// in which case we fall back to the default gas estimation below
let darkpool_addr = self.arbitrum_client.get_darkpool_client().address();
tx.set_from(darkpool_addr);
let gas = match self.arbitrum_client.client().estimate_gas(&tx, None /* block */).await {
Ok(gas) => gas,
Err(e) => {
warn!("Failed to estimate gas for gas sponsorship: {e}");
DEFAULT_GAS_ESTIMATION.into()
},
};

Ok(gas)
}
}
Loading

0 comments on commit 75c5359

Please sign in to comment.