Merge pull request #55 from propeller-heads/encoder/hr/ENG-4093-bin

This commit is contained in:
Harsh Vardhan Roy
2025-02-06 23:59:29 +05:30
committed by GitHub
8 changed files with 385 additions and 8 deletions

116
Cargo.lock generated
View File

@@ -647,6 +647,56 @@ dependencies = [
"libc", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.95" version = "1.0.95"
@@ -1298,6 +1348,46 @@ dependencies = [
"windows-targets", "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]] [[package]]
name = "coins-ledger" name = "coins-ledger"
version = "0.12.0" version = "0.12.0"
@@ -1321,6 +1411,12 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
] ]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]] [[package]]
name = "const-hex" name = "const-hex"
version = "1.14.0" version = "1.14.0"
@@ -2342,6 +2438,12 @@ version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@@ -2585,6 +2687,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde",
] ]
[[package]] [[package]]
@@ -3751,6 +3854,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.25.0" version = "0.25.0"
@@ -4237,6 +4346,7 @@ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
"chrono", "chrono",
"clap",
"dotenv", "dotenv",
"hex", "hex",
"lazy_static", "lazy_static",
@@ -4358,6 +4468,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "utoipa" name = "utoipa"
version = "4.2.3" version = "4.2.3"

View File

@@ -3,10 +3,15 @@ name = "tycho-execution"
version = "0.33.0" version = "0.33.0"
edition = "2021" edition = "2021"
[[bin]]
name = "tycho-encode"
path = "src/bin/tycho-encode.rs"
required-features = ["evm"]
[dependencies] [dependencies]
dotenv = "0.15.0" dotenv = "0.15.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
num-bigint = "0.4.6" num-bigint = { version = "0.4.6", features = ["serde"] }
hex = "0.4.3" hex = "0.4.3"
num-traits = "0.2.19" num-traits = "0.2.19"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
@@ -14,6 +19,7 @@ serde_json = "1.0.135"
thiserror = "1.0.69" thiserror = "1.0.69"
tokio = { version = "1.38.0", features = ["full"] } tokio = { version = "1.38.0", features = ["full"] }
chrono = "0.4.39" 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 = { version = "0.9.2", features = ["providers", "rpc-types-eth", "eip712", "signer-local"], optional = true }
alloy-sol-types = { version = "0.8.14", optional = true } alloy-sol-types = { version = "0.8.14", optional = true }

View File

@@ -3,16 +3,81 @@
![img.png](banner.png) ![img.png](banner.png)
Tycho Execution makes it easy to trade on different DEXs by handling the complex encoding for you. Instead of creating 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. Its 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. 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 '<solution_payload>' | tycho-encode
# Using custom config path
echo '<solution_payload>' | tycho-encode -c /path/to/your/config.json
# Using custom router address and config path
echo '<solution_payload>' | 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. 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. 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 create --name tycho-execution python=3.10
conda activate tycho-execution conda activate tycho-execution

50
src/bin/lib/cli.rs Normal file
View File

@@ -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<String>,
/// Path to the executor addresses configuration file
#[arg(short, default_value = DEFAULT_CONFIG_PATH)]
pub config_path: String,
}

68
src/bin/tycho-encode.rs Normal file
View File

@@ -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<dyn std::error::Error>> {
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<String>,
) -> Result<Value, Box<dyn std::error::Error>> {
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)),
}))
}

View File

@@ -2,6 +2,7 @@ mod errors;
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
pub mod evm; pub mod evm;
pub mod models; pub mod models;
pub mod serde_primitives;
pub mod strategy_encoder; pub mod strategy_encoder;
mod swap_encoder; mod swap_encoder;
pub mod tycho_encoder; pub mod tycho_encoder;

View File

@@ -1,15 +1,19 @@
use hex; use hex;
use num_bigint::BigUint; use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use tycho_core::{ use tycho_core::{
dto::{Chain as TychoCoreChain, ProtocolComponent}, dto::{Chain as TychoCoreChain, ProtocolComponent},
Bytes, 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 /// Represents a solution containing details describing an order, and instructions for filling
/// the order. /// the order.
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug, Deserialize, Serialize)]
pub struct Solution { pub struct Solution {
/// Address of the sender. /// Address of the sender.
pub sender: Bytes, pub sender: Bytes,
@@ -18,18 +22,22 @@ pub struct Solution {
/// The token being sold (exact in) or bought (exact out). /// The token being sold (exact in) or bought (exact out).
pub given_token: Bytes, pub given_token: Bytes,
/// Amount of the given token. /// Amount of the given token.
#[serde(with = "biguint_string")]
pub given_amount: BigUint, pub given_amount: BigUint,
/// The token being bought (exact in) or sold (exact out). /// The token being bought (exact in) or sold (exact out).
pub checked_token: Bytes, pub checked_token: Bytes,
/// False if the solution is an exact input solution. Currently only exact input solutions are /// False if the solution is an exact input solution. Currently only exact input solutions are
/// supported. /// supported.
#[serde(default)]
pub exact_out: bool, pub exact_out: bool,
/// If set, it will be applied to expected_amount /// If set, it will be applied to expected_amount
pub slippage: Option<f64>, pub slippage: Option<f64>,
/// Expected amount of the bought token (exact in) or sold token (exact out). /// Expected amount of the bought token (exact in) or sold token (exact out).
#[serde(with = "biguint_string_option")]
pub expected_amount: Option<BigUint>, pub expected_amount: Option<BigUint>,
/// Minimum amount to be checked for the solution to be valid. /// Minimum amount to be checked for the solution to be valid.
/// If not set, the check will not be performed. /// If not set, the check will not be performed.
#[serde(with = "biguint_string_option")]
pub check_amount: Option<BigUint>, pub check_amount: Option<BigUint>,
/// List of swaps to fulfill the solution. /// List of swaps to fulfill the solution.
pub swaps: Vec<Swap>, pub swaps: Vec<Swap>,
@@ -40,6 +48,7 @@ pub struct Solution {
/// If set to true, the solution will be encoded to be sent directly to the Executor and /// 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 /// skip the router. The user is responsible for managing necessary approvals and token
/// transfers. /// transfers.
#[serde(default)]
pub direct_execution: bool, 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` /// `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 /// means that the native token will be unwrapped after the last swap, before being sent to the
/// receiver. /// receiver.
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum NativeAction { pub enum NativeAction {
Wrap, Wrap,
Unwrap, Unwrap,
} }
/// Represents a swap operation to be performed on a pool. /// Represents a swap operation to be performed on a pool.
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Swap { pub struct Swap {
/// Protocol component from tycho indexer /// Protocol component from tycho indexer
pub component: ProtocolComponent, pub component: ProtocolComponent,
@@ -64,6 +74,7 @@ pub struct Swap {
/// Token being output from the pool. /// Token being output from the pool.
pub token_out: Bytes, pub token_out: Bytes,
/// Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%) /// Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)
#[serde(default)]
pub split: f64, pub split: f64,
} }

View File

@@ -0,0 +1,60 @@
use std::str::FromStr;
use num_bigint::BigUint;
use serde::{self, Deserialize, Deserializer, Serializer};
fn serialize_biguint<S>(value: &BigUint, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&value.to_string())
}
fn deserialize_biguint<'de, D>(deserializer: D) -> Result<BigUint, D::Error>
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<S>(value: &BigUint, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_biguint(value, serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<BigUint, D::Error>
where
D: Deserializer<'de>,
{
deserialize_biguint(deserializer)
}
}
pub mod biguint_string_option {
use super::*;
pub fn serialize<S>(value: &Option<BigUint>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(v) => serialize_biguint(v, serializer),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<BigUint>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|s| BigUint::from_str(&s).map_err(serde::de::Error::custom))
.transpose()
}
}