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 31, 2025
1 parent 24b1de5 commit d5d4521
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 25 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
5 changes: 5 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 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 All @@ -48,6 +49,7 @@ futures-util = "0.3"
metrics = "=0.22.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
thiserror = "1.0"
tracing = "0.1"
uuid = { version = "1.0", features = ["serde", "v4"] }
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
19 changes: 15 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::ExternalQuoteAssemblyQueryParams, 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,10 @@ async fn main() {
.and(warp::path::full())
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and(warp::query::<ExternalQuoteAssemblyQueryParams>())
.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, query_params, server: Arc<Server>| async move {
server.handle_external_quote_assembly_request(path, headers, body, query_params).await
});

let atomic_match_path = warp::path("v0")
Expand Down
212 changes: 208 additions & 4 deletions auth/auth-server/src/server/handle_external_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
//! 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::header::CONTENT_LENGTH;
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};

Expand All @@ -15,6 +25,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 +39,37 @@ 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(Debug, 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<String>,
}

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

/// Handle a proxied request
impl Server {
/// Handle an external quote request
Expand Down Expand Up @@ -64,14 +106,40 @@ impl Server {
path: warp::path::FullPath,
headers: warp::hyper::HeaderMap,
body: Bytes,
query_params: ExternalQuoteAssemblyQueryParams,
) -> Result<impl Reply, Rejection> {
// Serialize the path + query params for auth
let query_str = serde_urlencoded::to_string(&query_params).unwrap();
let auth_path = if query_str.is_empty() {
path.as_str().to_string()
} else {
format!("{}?{}", path.as_str(), query_str)
};

// Authorize the request
let key_desc = self.authorize_request(path.as_str(), &headers, &body).await?;
let key_desc = self.authorize_request(&auth_path, &headers, &body).await?;
self.check_bundle_rate_limit(key_desc.clone()).await?;

// Send the request to the relayer
let resp =
self.send_admin_request(Method::POST, path.as_str(), headers, body.clone()).await?;
let resp = if query_params.use_gas_sponsorship.unwrap_or(false) {
let original_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

info!("Redirecting match bundle through gas sponsor");
let refund_address = query_params
.refund_address
.map(|s| s.parse())
.transpose()
.map_err(AuthServerError::serde)?
.unwrap_or(Address::ZERO);

self.substitute_gas_sponsorship_response(original_resp, refund_address).await?
} else {
self.send_admin_request(Method::POST, path.as_str(), headers, body.clone()).await?
};

let resp_clone = resp.body().to_vec();
let server_clone = self.clone();
Expand Down Expand Up @@ -330,4 +398,140 @@ impl Server {

Ok(())
}

/// Substitute a quote assembly response with one that invokes gas
/// sponsorship
async fn substitute_gas_sponsorship_response(
&self,
resp: Response<Bytes>,
refund_address: Address,
) -> Result<Response<Bytes>, 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 =
Bytes::from(serde_json::to_vec(&external_match_resp).map_err(AuthServerError::serde)?);

let mut new_resp = warp::http::Response::new(body);
*new_resp.status_mut() = resp.status();

// Remove the old content length header so we don't overwrite the new one,
// it is incorrect since the calldata was mutated
let mut headers = resp.headers().clone();
headers.remove(CONTENT_LENGTH);

*new_resp.headers_mut() = headers;

Ok(new_resp)
}

/// 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 d5d4521

Please sign in to comment.