use alloy::{
primitives::{Address, U256},
signers::{local::PrivateKeySigner, SignerSync},
sol,
sol_types::{eip712_domain, SolStruct},
};
use tycho_simulation::tycho_common::Bytes;
use crate::{
encoding::{now_unix_secs, DEFAULT_DEADLINE_WINDOW_SECS},
SolveError,
};
pub(crate) const ENV_DISABLE_SLIPPAGE_TAKING_KEY: &str = "DISABLE_SLIPPAGE_TAKING_SIGNER_KEY";
sol! {
struct ClientFee {
uint32 clientFeeBps;
address clientFeeReceiver;
uint256 maxClientContribution;
uint256 deadline;
uint256 amountIn;
address tokenIn;
address tokenOut;
uint256 expectedAmountOut;
uint256 minAmountOut;
address receiver;
bytes swaps;
}
}
pub(crate) struct SwapIntent<'a> {
pub(crate) amount_in: U256,
pub(crate) token_in: Address,
pub(crate) token_out: Address,
pub(crate) expected_amount_out: U256,
pub(crate) min_amount_out: U256,
pub(crate) receiver: Address,
pub(crate) swaps: &'a [u8],
}
pub(crate) struct SignedClientFee {
pub(crate) deadline: u64,
pub(crate) signature: [u8; 65],
}
pub(crate) struct DisableSlippageTakingSigner {
signer: PrivateKeySigner,
chain_id: u64,
router_address: Address,
deadline_window_secs: u32,
}
impl DisableSlippageTakingSigner {
pub(crate) fn from_env(
chain_id: u64,
router_address: &Bytes,
) -> Result<Option<Self>, SolveError> {
let Ok(key) = std::env::var(ENV_DISABLE_SLIPPAGE_TAKING_KEY) else {
return Ok(None);
};
let signer = key
.parse::<PrivateKeySigner>()
.map_err(|e| {
SolveError::FailedEncoding(format!(
"invalid {ENV_DISABLE_SLIPPAGE_TAKING_KEY}: {e}"
))
})?;
let router = crate::rpc::to_address(router_address, "router address")
.map_err(SolveError::FailedEncoding)?;
Ok(Some(Self::new(signer, chain_id, router, DEFAULT_DEADLINE_WINDOW_SECS)))
}
pub(crate) fn new(
signer: PrivateKeySigner,
chain_id: u64,
router_address: Address,
deadline_window_secs: u32,
) -> Self {
Self { signer, chain_id, router_address, deadline_window_secs }
}
pub(crate) fn receiver(&self) -> Address {
self.signer.address()
}
pub(crate) fn sign_client_fee(
&self,
intent: &SwapIntent,
) -> Result<SignedClientFee, SolveError> {
let deadline = now_unix_secs()?.saturating_add(u64::from(self.deadline_window_secs));
let payload = ClientFee {
clientFeeBps: 0,
clientFeeReceiver: self.receiver(),
maxClientContribution: U256::ZERO,
deadline: U256::from(deadline),
amountIn: intent.amount_in,
tokenIn: intent.token_in,
tokenOut: intent.token_out,
expectedAmountOut: intent.expected_amount_out,
minAmountOut: intent.min_amount_out,
receiver: intent.receiver,
swaps: intent.swaps.to_vec().into(),
};
let domain = eip712_domain! {
name: "TychoRouter",
version: "1",
chain_id: self.chain_id,
verifying_contract: self.router_address,
};
let signing_hash = payload.eip712_signing_hash(&domain);
let signature = self
.signer
.sign_hash_sync(&signing_hash)
.map_err(|e| SolveError::FailedEncoding(format!("client fee signing failed: {e}")))?;
Ok(SignedClientFee { deadline, signature: signature.as_bytes() })
}
}
#[cfg(test)]
pub(crate) fn router_signing_hash(
fee_receiver: Address,
intent: &SwapIntent,
deadline: u64,
chain_id: u64,
router: Address,
) -> alloy::primitives::B256 {
use alloy::{primitives::keccak256, sol_types::SolValue};
let type_hash = keccak256(
b"ClientFee(uint32 clientFeeBps,address clientFeeReceiver,\
uint256 maxClientContribution,uint256 deadline,\
uint256 amountIn,address tokenIn,address tokenOut,\
uint256 expectedAmountOut,uint256 minAmountOut,address receiver,bytes swaps)",
);
let struct_hash = keccak256(
(
type_hash,
U256::ZERO,
fee_receiver,
U256::ZERO,
U256::from(deadline),
intent.amount_in,
intent.token_in,
intent.token_out,
intent.expected_amount_out,
intent.min_amount_out,
intent.receiver,
keccak256(intent.swaps),
)
.abi_encode(),
);
let domain_type_hash = keccak256(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
);
let domain_separator = keccak256(
(
domain_type_hash,
keccak256(b"TychoRouter"),
keccak256(b"1"),
U256::from(chain_id),
router,
)
.abi_encode(),
);
let mut data = [0u8; 66];
data[0] = 0x19;
data[1] = 0x01;
data[2..34].copy_from_slice(domain_separator.as_slice());
data[34..66].copy_from_slice(struct_hash.as_slice());
keccak256(data)
}
#[cfg(test)]
mod tests {
use alloy::primitives::Signature;
use super::*;
const SIGNER_KEY: &str = "0x2222222222222222222222222222222222222222222222222222222222222222";
const ROUTER: Address = Address::repeat_byte(0x99);
const CHAIN_ID: u64 = 1;
fn test_signer() -> DisableSlippageTakingSigner {
DisableSlippageTakingSigner::new(
SIGNER_KEY.parse().unwrap(),
CHAIN_ID,
ROUTER,
DEFAULT_DEADLINE_WINDOW_SECS,
)
}
fn test_intent(swaps: &[u8]) -> SwapIntent<'_> {
SwapIntent {
amount_in: U256::from(1_000_000u64),
token_in: Address::repeat_byte(0x01),
token_out: Address::repeat_byte(0x02),
expected_amount_out: U256::from(990_000u64),
min_amount_out: U256::from(980_000u64),
receiver: Address::repeat_byte(0x03),
swaps,
}
}
#[test]
fn test_sign_client_fee_against_router_signing_hash() {
let signer = test_signer();
let intent = test_intent(&[0xAB; 40]);
let signed = signer.sign_client_fee(&intent).unwrap();
let signature = Signature::try_from(&signed.signature[..]).unwrap();
let recovered = signature
.recover_address_from_prehash(&router_signing_hash(
signer.receiver(),
&intent,
signed.deadline,
CHAIN_ID,
ROUTER,
))
.unwrap();
assert_eq!(recovered, signer.receiver());
assert!(matches!(signed.signature[64], 27 | 28));
}
#[test]
fn test_sign_client_fee_deadline_window() {
let before = now_unix_secs().unwrap();
let signed = test_signer()
.sign_client_fee(&test_intent(&[0xAB; 40]))
.unwrap();
let window = u64::from(DEFAULT_DEADLINE_WINDOW_SECS);
assert!(
signed.deadline >= before + window,
"deadline {} predates the window opened at {before}",
signed.deadline
);
assert!(
signed.deadline <= now_unix_secs().unwrap() + window,
"deadline {} outlives the window",
signed.deadline
);
}
}