diff --git a/Cargo.lock b/Cargo.lock index 829efb8..7ea2e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -647,6 +647,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -1298,6 +1348,46 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "clap" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "coins-ledger" version = "0.12.0" @@ -1321,6 +1411,12 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "const-hex" version = "1.14.0" @@ -2342,6 +2438,12 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -2585,6 +2687,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -3751,6 +3854,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.25.0" @@ -4237,6 +4346,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "chrono", + "clap", "dotenv", "hex", "lazy_static", @@ -4358,6 +4468,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "utoipa" version = "4.2.3" diff --git a/Cargo.toml b/Cargo.toml index 9db5661..330c0b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,15 @@ name = "tycho-execution" version = "0.33.0" edition = "2021" +[[bin]] +name = "tycho-encode" +path = "src/bin/tycho-encode.rs" +required-features = ["evm"] + [dependencies] dotenv = "0.15.0" lazy_static = "1.4.0" -num-bigint = "0.4.6" +num-bigint = { version = "0.4.6", features = ["serde"] } hex = "0.4.3" num-traits = "0.2.19" serde = { version = "1.0.217", features = ["derive"] } @@ -14,6 +19,7 @@ serde_json = "1.0.135" thiserror = "1.0.69" tokio = { version = "1.38.0", features = ["full"] } chrono = "0.4.39" +clap = { version = "4.5.3", features = ["derive"] } alloy = { version = "0.9.2", features = ["providers", "rpc-types-eth", "eip712", "signer-local"], optional = true } alloy-sol-types = { version = "0.8.14", optional = true } diff --git a/README.md b/README.md index 692109b..3910a29 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,81 @@ ![img.png](banner.png) Tycho Execution makes it easy to trade on different DEXs by handling the complex encoding for you. Instead of creating -custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It’s +custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It's designed to be safe, straightforward, and quick to set up, so anyone can start trading without extra effort. -# Contract Analysis +## Bin Usage Guide + +### Installation + +First, build and install the binary: +```bash +# Build the project +cargo build --release + +# Install the binary to your system +cargo install --path . +``` + +After installation, the `tycho-encode` command will be available to use from any directory in your terminal. The command accepts the following options: + +- `-c`: Path to the executor addresses configuration file (defaults to `src/encoding/config/executor_addresses.json`) +- `-p`: Private key for signing approvals (required when direct_execution is false) +- `ROUTER_ADDRESS`: Router contract address (defaults to `0xaa820C29648D5EA543d712cC928377Bd7206a0E7`) + + +### Encoding Transactions + +To encode a transaction, you can pipe a JSON payload to the binary: + +```bash +# Using default config path +echo '' | tycho-encode + +# Using custom config path +echo '' | tycho-encode -c /path/to/your/config.json + +# Using custom router address and config path +echo '' | tycho-encode -c /path/to/your/config.json 0x1234...5678 +``` + +#### Example + +Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: + +```bash +echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":true}' | tycho-encode +``` + + +#### JSON Payload Structure: Solution struct + +The `Solution` struct is composed of the following fields: + +- `sender`: The address initiating the transaction +- `receiver`: The address receiving the output tokens +- `given_token`: The address of the input token (e.g., WETH) +- `given_amount`: The amount of input tokens (in wei) +- `checked_token`: The address of the output token (e.g., DAI) +- `exact_out`: Boolean indicating if this is an exact output swap +- `slippage`: The maximum allowed slippage (e.g., 0.01 for 1%) +- `expected_amount`: The expected output amount +- `check_amount`: The minimum acceptable output amount (accounting for slippage) +- `swaps`: Array of swap steps, each containing: + - `component`: Details about the DEX/protocol being used + - `token_in`: Input token address for this step + - `token_out`: Output token address for this step + - `split`: Proportion of tokens to route through this step (1.0 = 100%) +- `router_address`: The address of the protocol's router contract +- `direct_execution`: Boolean indicating if the transaction should be executed directly + +## Contract Analysis We use [Slither](https://github.com/crytic/slither) to detect any potential vulnerabilities in our contracts. To run locally, simply install Slither in your conda env and run it inside the foundry directory. -``` +```bash conda create --name tycho-execution python=3.10 conda activate tycho-execution diff --git a/src/bin/lib/cli.rs b/src/bin/lib/cli.rs new file mode 100644 index 0000000..25e8786 --- /dev/null +++ b/src/bin/lib/cli.rs @@ -0,0 +1,50 @@ +pub use clap::Parser; +pub const DEFAULT_ROUTER_ADDRESS: &str = "0xaa820C29648D5EA543d712cC928377Bd7206a0E7"; +pub const DEFAULT_CONFIG_PATH: &str = "src/encoding/config/executor_addresses.json"; + +#[derive(Parser)] +/// Encode swap transactions for the Tycho router +/// +/// Reads a JSON object from stdin with the following structure: +/// ```json +/// { +/// "sender": "0x...", +/// "receiver": "0x...", +/// "given_token": "0x...", +/// "given_amount": "123...", +/// "checked_token": "0x...", +/// "exact_out": false, +/// "slippage": 0.01, +/// "expected_amount": "123...", +/// "check_amount": "123...", +/// "swaps": [{ +/// "component": { +/// "id": "...", +/// "protocol_system": "...", +/// "protocol_type_name": "...", +/// "chain": "ethereum", +/// "tokens": ["0x..."], +/// "contract_ids": ["0x..."], +/// "static_attributes": {"key": "0x..."} +/// }, +/// "token_in": "0x...", +/// "token_out": "0x...", +/// "split": 1.0 +/// }], +/// "router_address": "0x...", +/// "direct_execution": false +/// } +/// ``` +pub struct Cli { + /// Router contract address to use for encoding transactions + #[arg(default_value = DEFAULT_ROUTER_ADDRESS)] + pub router_address: String, + + /// Private key for signing approvals (required when direct_execution is false) + #[arg(short)] + pub private_key: Option, + + /// Path to the executor addresses configuration file + #[arg(short, default_value = DEFAULT_CONFIG_PATH)] + pub config_path: String, +} diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs new file mode 100644 index 0000000..62bb74d --- /dev/null +++ b/src/bin/tycho-encode.rs @@ -0,0 +1,68 @@ +use std::io::{self, Read}; + +use clap::Parser; +use serde_json::Value; +use tycho_core::dto::Chain; +use tycho_execution::encoding::{ + evm::{ + strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry, + tycho_encoder::EVMTychoEncoder, + }, + models::Solution, + strategy_encoder::StrategyEncoderRegistry, + tycho_encoder::TychoEncoder, +}; + +mod lib { + pub mod cli; +} + +use lib::cli::Cli; + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + // Read from stdin until EOF + let mut buffer = String::new(); + io::stdin() + .read_to_string(&mut buffer) + .map_err(|e| format!("Failed to read from stdin: {}", e))?; + + if buffer.trim().is_empty() { + return Err("No input provided. Expected JSON input on stdin.".into()); + } + + // Encode the solution + let encoded = encode_swaps(&buffer, &cli.router_address, &cli.config_path, cli.private_key)?; + + // Output the encoded result as JSON to stdout + println!( + "{}", + serde_json::to_string(&encoded) + .map_err(|e| format!("Failed to serialize output: {}", e))? + ); + + Ok(()) +} + +fn encode_swaps( + input: &str, + router_address: &str, + config_path: &str, + private_key: Option, +) -> Result> { + let solution: Solution = serde_json::from_str(input)?; + let chain = Chain::Ethereum; + + let strategy_selector = + EVMStrategyEncoderRegistry::new(chain.into(), config_path, private_key)?; + let encoder = + EVMTychoEncoder::new(strategy_selector, router_address.to_string(), chain.into())?; + let transactions = encoder.encode_router_calldata(vec![solution])?; + + Ok(serde_json::json!({ + "to": format!("0x{}", hex::encode(&transactions[0].to)), + "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), + "data": format!("0x{}", hex::encode(&transactions[0].data)), + })) +} diff --git a/src/encoding/mod.rs b/src/encoding/mod.rs index 502e336..be17c4a 100644 --- a/src/encoding/mod.rs +++ b/src/encoding/mod.rs @@ -2,6 +2,7 @@ mod errors; #[cfg(feature = "evm")] pub mod evm; pub mod models; +pub mod serde_primitives; pub mod strategy_encoder; mod swap_encoder; pub mod tycho_encoder; diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 3aa278f..6a0e937 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -1,15 +1,19 @@ use hex; use num_bigint::BigUint; +use serde::{Deserialize, Serialize}; use tycho_core::{ dto::{Chain as TychoCoreChain, ProtocolComponent}, Bytes, }; -use crate::encoding::errors::EncodingError; +use crate::encoding::{ + errors::EncodingError, + serde_primitives::{biguint_string, biguint_string_option}, +}; /// Represents a solution containing details describing an order, and instructions for filling /// the order. -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, Deserialize, Serialize)] pub struct Solution { /// Address of the sender. pub sender: Bytes, @@ -18,18 +22,22 @@ pub struct Solution { /// The token being sold (exact in) or bought (exact out). pub given_token: Bytes, /// Amount of the given token. + #[serde(with = "biguint_string")] pub given_amount: BigUint, /// The token being bought (exact in) or sold (exact out). pub checked_token: Bytes, /// False if the solution is an exact input solution. Currently only exact input solutions are /// supported. + #[serde(default)] pub exact_out: bool, /// If set, it will be applied to expected_amount pub slippage: Option, /// Expected amount of the bought token (exact in) or sold token (exact out). + #[serde(with = "biguint_string_option")] pub expected_amount: Option, /// Minimum amount to be checked for the solution to be valid. /// If not set, the check will not be performed. + #[serde(with = "biguint_string_option")] pub check_amount: Option, /// List of swaps to fulfill the solution. pub swaps: Vec, @@ -40,6 +48,7 @@ pub struct Solution { /// If set to true, the solution will be encoded to be sent directly to the Executor and /// skip the router. The user is responsible for managing necessary approvals and token /// transfers. + #[serde(default)] pub direct_execution: bool, } @@ -48,14 +57,15 @@ pub struct Solution { /// `Wrap` means that the native token will be wrapped before the first swap, and `Unwrap` /// means that the native token will be unwrapped after the last swap, before being sent to the /// receiver. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] pub enum NativeAction { Wrap, Unwrap, } /// Represents a swap operation to be performed on a pool. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Swap { /// Protocol component from tycho indexer pub component: ProtocolComponent, @@ -64,6 +74,7 @@ pub struct Swap { /// Token being output from the pool. pub token_out: Bytes, /// Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%) + #[serde(default)] pub split: f64, } diff --git a/src/encoding/serde_primitives.rs b/src/encoding/serde_primitives.rs new file mode 100644 index 0000000..9898815 --- /dev/null +++ b/src/encoding/serde_primitives.rs @@ -0,0 +1,60 @@ +use std::str::FromStr; + +use num_bigint::BigUint; +use serde::{self, Deserialize, Deserializer, Serializer}; + +fn serialize_biguint(value: &BigUint, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&value.to_string()) +} + +fn deserialize_biguint<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + BigUint::from_str(&s).map_err(serde::de::Error::custom) +} + +pub mod biguint_string { + use super::*; + + pub fn serialize(value: &BigUint, serializer: S) -> Result + where + S: Serializer, + { + serialize_biguint(value, serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_biguint(deserializer) + } +} + +pub mod biguint_string_option { + use super::*; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(v) => serialize_biguint(v, serializer), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer)? + .map(|s| BigUint::from_str(&s).map_err(serde::de::Error::custom)) + .transpose() + } +}