Merge branch 'main' into feat/maverick-v2-executor
This commit is contained in:
147
CHANGELOG.md
147
CHANGELOG.md
@@ -1,3 +1,150 @@
|
||||
## [0.84.0](https://github.com/propeller-heads/tycho-execution/compare/0.83.0...0.84.0) (2025-04-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Clear transient storage after swap call ([1b003dc](https://github.com/propeller-heads/tycho-execution/commit/1b003dc483d0e6cc01c5fd6d46ea9f28fc1c0aa6))
|
||||
|
||||
## [0.83.0](https://github.com/propeller-heads/tycho-execution/compare/0.82.1...0.83.0) (2025-04-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add security check for callback selector ([4de1d10](https://github.com/propeller-heads/tycho-execution/commit/4de1d104062d4aefbde22031d9f31884be5d49ad))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove tload from executor ([3fb17c7](https://github.com/propeller-heads/tycho-execution/commit/3fb17c71da192463b0c6b15dea9a2bae47832ef5))
|
||||
|
||||
## [0.82.1](https://github.com/propeller-heads/tycho-execution/compare/0.82.0...0.82.1) (2025-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Make clippy happy after new format! format ([7bf0b48](https://github.com/propeller-heads/tycho-execution/commit/7bf0b48ea6ac536ba7d52b1e2e3bfb50deecd1cf))
|
||||
|
||||
## [0.82.0](https://github.com/propeller-heads/tycho-execution/compare/0.81.0...0.82.0) (2025-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* (WIP) Support selection of transfer into router ([a301a1c](https://github.com/propeller-heads/tycho-execution/commit/a301a1cef3897ca1feeb6ec2245a2602fe649c9c))
|
||||
* Add sequential swap methods ([3ae9d3a](https://github.com/propeller-heads/tycho-execution/commit/3ae9d3ad670e760e2f7d0cbe81e5b19b7e7a3dae))
|
||||
* Add SequentialSwap integration test with regular approvals ([6e7bf3c](https://github.com/propeller-heads/tycho-execution/commit/6e7bf3c019c32d586d406bf420daa0080dcda1fd))
|
||||
* Add SingleSwap integration test and fix bug in method signatures ([df1c05e](https://github.com/propeller-heads/tycho-execution/commit/df1c05ea00d23855787dd2df45e7937ec9fc23ec))
|
||||
* Add TokenTransfer class to BalancerV2 ([3a73fef](https://github.com/propeller-heads/tycho-execution/commit/3a73fef9334f3da49247f666866e5b76d44c40dd))
|
||||
* Add TokenTransfer class to Curve ([462be54](https://github.com/propeller-heads/tycho-execution/commit/462be5463b77b5a69289df06738053ea5f3b3ce8))
|
||||
* Add transfer out for Curve ([328a281](https://github.com/propeller-heads/tycho-execution/commit/328a281a44e033a72e7bb8991f689c736718f098))
|
||||
* Add transfer out for Uniswap V4 ([ec87969](https://github.com/propeller-heads/tycho-execution/commit/ec87969aa6e938a4e0d5bdfa0d6aaae1c49b56fd))
|
||||
* Allow for token_in_already_in_router ([df92be8](https://github.com/propeller-heads/tycho-execution/commit/df92be887573b297b22f2e01317305f2e5bb7e75))
|
||||
* allow to pass msg.sender to USV3 callback ([a37805d](https://github.com/propeller-heads/tycho-execution/commit/a37805d0469ee67b2685b7f9b83bdd042bc6d6d9))
|
||||
* allow to pass msg.sender to USV3 callback ([8969186](https://github.com/propeller-heads/tycho-execution/commit/8969186654ec5db96bdcaab0099b883f1ed0ba63))
|
||||
* Decode single and sequential swaps in LibSwap ([1dad4af](https://github.com/propeller-heads/tycho-execution/commit/1dad4afb6b91cf86ea000afeaeba8882af73b713))
|
||||
* Delete EVMStrategyEncoder (this is now unnecessary) ([6430c99](https://github.com/propeller-heads/tycho-execution/commit/6430c99d7665e855d950dde7f199e9f69cf1f642))
|
||||
* Do not use V4Router for uniswap v4 ([cd608cb](https://github.com/propeller-heads/tycho-execution/commit/cd608cb8e94b17c06e5df5f15672612cfa4fcb58))
|
||||
* ExecutorTransferMethods helper contract ([147ba68](https://github.com/propeller-heads/tycho-execution/commit/147ba68392bb776e8a48d7818edb567e3d66b5de))
|
||||
* ExecutorTransferMethods in UniswapV3Executor ([3189139](https://github.com/propeller-heads/tycho-execution/commit/31891397c725f71343787bf8e081fa45174a23a1))
|
||||
* ExecutorTransferMethods in UniswapV3Executor ([3890099](https://github.com/propeller-heads/tycho-execution/commit/389009901ed0eb8c380c8d1a2f0a2a0668295cf5))
|
||||
* Fix rollFork usage for Ekubo test ([d7b5e6d](https://github.com/propeller-heads/tycho-execution/commit/d7b5e6dba5e0ae8fbf67dfb102fca51477f0127b))
|
||||
* Make USV3 callback work with direct executor call ([5562dd2](https://github.com/propeller-heads/tycho-execution/commit/5562dd210eb31efcb855e29d18245248e9e83411))
|
||||
* No more fee taking ([6f2e5ac](https://github.com/propeller-heads/tycho-execution/commit/6f2e5ac10e05df7c66cf9ef2ede19656c17d0e9b))
|
||||
* Optimize transfer to first pool ([59a80dc](https://github.com/propeller-heads/tycho-execution/commit/59a80dc3929ce75910dce13ba837a8c2445048fb))
|
||||
* Proper USV2Executor transfer decoding + tests ([e8f56ff](https://github.com/propeller-heads/tycho-execution/commit/e8f56ff08860f3f7d248ba60bfbbd130f3e12082))
|
||||
* Proper USV2Executor transfer decoding + tests ([ca1d474](https://github.com/propeller-heads/tycho-execution/commit/ca1d474f0874cc24fc713191470d23a72b9d3e04))
|
||||
* Proper USV3Executor transfer decoding + tests ([e3ac394](https://github.com/propeller-heads/tycho-execution/commit/e3ac394d27b8adaeb0aa16ffffb286fc31486ef1))
|
||||
* Refactor callback for pancakeV3 and Ekubo to use transient storage ([deb10b8](https://github.com/propeller-heads/tycho-execution/commit/deb10b82deba12ee7d68d586ba04498a3f3ca208))
|
||||
* Refactor callback to use transient storage ([af44956](https://github.com/propeller-heads/tycho-execution/commit/af449562b0c4038cf666194a52eea582855495fc))
|
||||
* Refactor TychoEncoder ([8b4b79b](https://github.com/propeller-heads/tycho-execution/commit/8b4b79b353a34011fb25877273d78962bdce60f6))
|
||||
* Rename split swap interfaces ([7fc008a](https://github.com/propeller-heads/tycho-execution/commit/7fc008a7180bcc6439ab2b8d3bc3d3af75ee92fd))
|
||||
* sequential swap solution validation ([0d8150e](https://github.com/propeller-heads/tycho-execution/commit/0d8150e22f2a354a41583a722bd4eba60ab1de59))
|
||||
* Single swap methods. ([6434566](https://github.com/propeller-heads/tycho-execution/commit/64345663d085114310e71d3d1bd316858626bf83))
|
||||
* SingleSwapStrategyEncoder ([11a05e4](https://github.com/propeller-heads/tycho-execution/commit/11a05e4f18688eb0fce0a2bc2c171c9e0a355177))
|
||||
* SingleSwapStrategyEncoder ([5d586c2](https://github.com/propeller-heads/tycho-execution/commit/5d586c25e3e084511b34677684cd1bb71e249fcc))
|
||||
* SingleSwapStrategyEncoder ([a5f07a2](https://github.com/propeller-heads/tycho-execution/commit/a5f07a25ef3edc099f45ab5d4da5acce694b155a))
|
||||
* Support in between swaps optimizations ([efe12cf](https://github.com/propeller-heads/tycho-execution/commit/efe12cfcd671ec1a93c106dcacf4535ebb54923e))
|
||||
* Support out transfer straight to the receiver ([9bcb58e](https://github.com/propeller-heads/tycho-execution/commit/9bcb58e5aa4e0d9b8483245da133951b39838e5b))
|
||||
* Support returning values from the callback ([fb35a53](https://github.com/propeller-heads/tycho-execution/commit/fb35a5305a91c377b9617965b4ed36c06be3bd42))
|
||||
* Support using the TransferType in uniswap v4 ([4a61de5](https://github.com/propeller-heads/tycho-execution/commit/4a61de56b7dd75e3eae9479cd6cdcefa39990322))
|
||||
* **tycho-router-encoder:** Select strategy depending on the solution ([f5e712e](https://github.com/propeller-heads/tycho-execution/commit/f5e712e0ffa898080069583cd080decca3f7e6a5))
|
||||
* Update tycho-encode bin with new arguments ([aba280a](https://github.com/propeller-heads/tycho-execution/commit/aba280acd084b1573a17cd5db7e1bb5875ee53c9))
|
||||
* Use TokenTransfer optimization helper in Ekubo ([d9066d0](https://github.com/propeller-heads/tycho-execution/commit/d9066d0a099d9c710b64a3ca39af729d83c91cf1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add slither ignore for loop call ([860bba4](https://github.com/propeller-heads/tycho-execution/commit/860bba4b7a255a33ba4f70f8c6d4a8358e2b2f6a))
|
||||
* After merge format fixes ([2b48293](https://github.com/propeller-heads/tycho-execution/commit/2b48293b3d7e92adaf76c1053b566e313192ef22))
|
||||
* After merge test fixes ([02cbb67](https://github.com/propeller-heads/tycho-execution/commit/02cbb67b3844a674fc52457967d123566fa77bf4))
|
||||
* After rebase fixes ([8145f41](https://github.com/propeller-heads/tycho-execution/commit/8145f416b7a12c52e27a1b3b6399d52625d992e5))
|
||||
* bad merge ([7e98145](https://github.com/propeller-heads/tycho-execution/commit/7e98145ad7d96b22c087b306eed38142396d062a))
|
||||
* Bring back receiver address zero check ([d447551](https://github.com/propeller-heads/tycho-execution/commit/d447551e20c42cecd47c3005b430924f6d8aff5f))
|
||||
* Calldata size for Ekubo pay callback ([be65c89](https://github.com/propeller-heads/tycho-execution/commit/be65c890bb18440ab22f442e5fea3d242300e5e2))
|
||||
* Changes after rebase ([d5d6e37](https://github.com/propeller-heads/tycho-execution/commit/d5d6e37041c316c460a3cf216c71eae1987a953f))
|
||||
* Configurable fee on USV2 executor. ([4f9785f](https://github.com/propeller-heads/tycho-execution/commit/4f9785fdacb309feb689abc874f458eb06540a1b))
|
||||
* Conscious slither silencing ([4a20fa6](https://github.com/propeller-heads/tycho-execution/commit/4a20fa621557d754cb677af41aa72de5cd7a6ffb))
|
||||
* consider wrapping scenario when getting transfer type ([dbc9042](https://github.com/propeller-heads/tycho-execution/commit/dbc9042a2f4fbe8377cee8b554c8a15da1be8a89))
|
||||
* Fix after merge with main ([fff8ef0](https://github.com/propeller-heads/tycho-execution/commit/fff8ef0d87d7c1071035cb62e76894cfb18305a7))
|
||||
* Fix executor address in test and remove duplicated test ([2f81b16](https://github.com/propeller-heads/tycho-execution/commit/2f81b167d428c8947b795929db2a96010efa9817))
|
||||
* Fix integration tests with transfer in method support ([d3ff9fd](https://github.com/propeller-heads/tycho-execution/commit/d3ff9fd0e26081ae80de05623fad188fe66c4959))
|
||||
* fix slither CI action ([8f23463](https://github.com/propeller-heads/tycho-execution/commit/8f2346330a0ef3a47ea887da9d5217c7b14ea209))
|
||||
* Fixes after merge with feature branch ([26ec308](https://github.com/propeller-heads/tycho-execution/commit/26ec30852d8bf4d94678e5d1071710ca0421dda6))
|
||||
* Integration tests after merge ([3f6bc56](https://github.com/propeller-heads/tycho-execution/commit/3f6bc5643e003792943f8a151bef44bea01c062d))
|
||||
* No more EVMStrategyEncoder ([56d3eee](https://github.com/propeller-heads/tycho-execution/commit/56d3eee6184b705bf1bfd091b23dfee12ab641bc))
|
||||
* Post rebase fixes ([d024fe2](https://github.com/propeller-heads/tycho-execution/commit/d024fe240b49d3a27d135fe340bee83a9a7f3f87))
|
||||
* Prevent multiple callbacks ([f91b101](https://github.com/propeller-heads/tycho-execution/commit/f91b101a94c85b78b5b3033915c74649d6916070))
|
||||
* properly add ekubo_v2 to constants ([11886b3](https://github.com/propeller-heads/tycho-execution/commit/11886b3ac1438eed63841edf697fe9b122a75483))
|
||||
* Remove router_address from Solution object ([fcedd39](https://github.com/propeller-heads/tycho-execution/commit/fcedd39f30033a4ce569f5ecc9a995526f164cdc))
|
||||
* Rename constants and update docstrings for clarity ([244b7d3](https://github.com/propeller-heads/tycho-execution/commit/244b7d3482da7bd99e5b78b325b135d2adce09f7))
|
||||
* test fix after rebase ([61c0163](https://github.com/propeller-heads/tycho-execution/commit/61c0163bee363bcd14644f8d0a8616e4970b4fdc))
|
||||
* Test+formatting fixes after rebase. ([e96ea1b](https://github.com/propeller-heads/tycho-execution/commit/e96ea1b10b84cb22d6732329a668f9ddd62b3276))
|
||||
* Tighten max feeBps in USV2 executor ([af68016](https://github.com/propeller-heads/tycho-execution/commit/af68016223cb74b4896ae627be798e6d211ef4e6))
|
||||
* TransferType renaming after rebase ([cf0300d](https://github.com/propeller-heads/tycho-execution/commit/cf0300dd72f83e9e39bad62223f4b327760ccf22))
|
||||
* unsupported protocols for chained swaps are always unsupported ([8aa5b08](https://github.com/propeller-heads/tycho-execution/commit/8aa5b08b419e45de5f69c72904f939881fe912ba))
|
||||
* USV3 encoding/decoding after rebase ([f3c4128](https://github.com/propeller-heads/tycho-execution/commit/f3c4128eda18dce0d573f9d8e0cb75100d922d11))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "chore(release): 1.0.0 [skip ci]" ([07be6b9](https://github.com/propeller-heads/tycho-execution/commit/07be6b932cf39cf94c4e3d973b09532fbea8601d))
|
||||
* Revert "chore(release): 1.0.1 [skip ci]" ([a5e0d16](https://github.com/propeller-heads/tycho-execution/commit/a5e0d16c8c84d9f2095d8b51caf22e7c71a9db1c))
|
||||
* Revert "chore(release): 1.0.2 [skip ci]" ([2bfd780](https://github.com/propeller-heads/tycho-execution/commit/2bfd7806f885d209c119e795604ff629563b57a1))
|
||||
* Revert "chore(release): 1.1.0 [skip ci]" ([e15ed9b](https://github.com/propeller-heads/tycho-execution/commit/e15ed9b16e248d5bb2347f43b3624584d262a8ae))
|
||||
|
||||
## [0.81.0](https://github.com/propeller-heads/tycho-execution/compare/0.80.0...0.81.0) (2025-04-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* update tycho-common version to 0.66.4 ([134c73e](https://github.com/propeller-heads/tycho-execution/commit/134c73e82be74fb5590e19c3d9b27304043bbbd8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add slither disable after slither actions update ([20573cb](https://github.com/propeller-heads/tycho-execution/commit/20573cbaf320ba99aa721e6e76a69447ab3f9694))
|
||||
|
||||
## [0.80.0](https://github.com/propeller-heads/tycho-execution/compare/0.79.0...0.80.0) (2025-04-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Redeploy balancer with forceApprove fix for USDT ([a6b0f8d](https://github.com/propeller-heads/tycho-execution/commit/a6b0f8d1f67a49848e90d2c4102195c4ac40c5a8))
|
||||
|
||||
## [0.79.0](https://github.com/propeller-heads/tycho-execution/compare/0.78.1...0.79.0) (2025-04-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new CurveExecutor address ([916c2b7](https://github.com/propeller-heads/tycho-execution/commit/916c2b7dba2c1c424efcbf884932a05427816cf8))
|
||||
* Deploy Curve Executor ([5d4d6d1](https://github.com/propeller-heads/tycho-execution/commit/5d4d6d1ff891766c067e3ff6355ffbb5c50bbf16))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Checksum curve pool addresses ([9e68ab8](https://github.com/propeller-heads/tycho-execution/commit/9e68ab8b0127831ee9dbc1f168e8aac9e28991c0))
|
||||
* Support pools that hold ETH but the coin is WETH ([2e8392a](https://github.com/propeller-heads/tycho-execution/commit/2e8392ab40c6c0e99089fae71873755dedb6e925))
|
||||
* Use forceApprove instead of regular Approve ([c963f3b](https://github.com/propeller-heads/tycho-execution/commit/c963f3b2f61e9d1a6e333149b091c3df90fd857b))
|
||||
|
||||
## [0.78.1](https://github.com/propeller-heads/tycho-execution/compare/0.78.0...0.78.1) (2025-04-09)
|
||||
|
||||
|
||||
|
||||
1233
Cargo.lock
generated
1233
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tycho-execution"
|
||||
version = "0.78.1"
|
||||
version = "0.84.0"
|
||||
edition = "2021"
|
||||
description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors."
|
||||
repository = "https://github.com/propeller-heads/tycho-execution"
|
||||
@@ -32,7 +32,7 @@ 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 }
|
||||
alloy-primitives = { version = "0.8.9", optional = true }
|
||||
tycho-common = "0.64.1"
|
||||
tycho-common = "^0.66.4"
|
||||
once_cell = "1.20.2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -12,3 +12,7 @@ For complete documentation, see Tycho docs [here](https://docs.propellerheads.xy
|
||||
|
||||
To get started on encoding, have a look at our [Encoding example](examples/encoding-example/README.md).
|
||||
For a complete example please refer to the [Tycho Quickstart guide](https://docs.propellerheads.xyz/tycho).
|
||||
|
||||
## Report a Vulnerability
|
||||
|
||||
Contact [security@propellerheads.xyz](mailto:security@propellerheads.xyz)
|
||||
@@ -6,9 +6,9 @@
|
||||
"uniswap_v3": "0xdD8559c917393FC8DD2b4dD289c52Ff445fDE1B0",
|
||||
"pancakeswap_v3": "0x4929B619A8F0D9c06ed0FfD497636580D823F65d",
|
||||
"uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70",
|
||||
"vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91",
|
||||
"vm:balancer_v2": "0x2380a9ff20565191b67cd66914cf5151434d71f5",
|
||||
"ekubo_v2": "0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D",
|
||||
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211"
|
||||
"vm:curve": "0x2751999a30A0026c909c4f1EB92d123254CABa7F"
|
||||
},
|
||||
"tenderly_ethereum": {
|
||||
"uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9",
|
||||
"uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
|
||||
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
|
||||
"ekubo_v2": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7",
|
||||
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
|
||||
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ use tycho_common::{
|
||||
Bytes,
|
||||
};
|
||||
use tycho_execution::encoding::{
|
||||
evm::encoder_builder::EVMEncoderBuilder,
|
||||
evm::encoder_builders::TychoRouterEncoderBuilder,
|
||||
models::{Solution, Swap},
|
||||
tycho_encoder::TychoEncoder,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -19,10 +18,9 @@ fn main() {
|
||||
.expect("Failed to create user address");
|
||||
|
||||
// Initialize the encoder
|
||||
let encoder = EVMEncoderBuilder::new()
|
||||
let encoder = TychoRouterEncoderBuilder::new()
|
||||
.chain(Chain::Ethereum)
|
||||
.initialize_tycho_router_with_permit2(swapper_pk)
|
||||
.expect("Failed to create encoder builder")
|
||||
.swapper_pk(swapper_pk)
|
||||
.build()
|
||||
.expect("Failed to build encoder");
|
||||
|
||||
@@ -64,7 +62,7 @@ fn main() {
|
||||
|
||||
// Encode the solution
|
||||
let tx = encoder
|
||||
.encode_router_calldata(vec![solution.clone()])
|
||||
.encode_calldata(vec![solution.clone()])
|
||||
.expect("Failed to encode router calldata")[0]
|
||||
.clone();
|
||||
println!(" ====== Simple swap WETH -> USDC ======");
|
||||
@@ -137,7 +135,7 @@ fn main() {
|
||||
|
||||
// Encode the solution
|
||||
let complex_tx = encoder
|
||||
.encode_router_calldata(vec![complex_solution])
|
||||
.encode_calldata(vec![complex_solution])
|
||||
.expect("Failed to encode router calldata")[0]
|
||||
.clone();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ evm_version = 'cancun'
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
via_ir = true
|
||||
fs_permissions = [{ access = "read", path = "./test/assets" }]
|
||||
|
||||
[profile.production]
|
||||
src = 'src'
|
||||
@@ -21,6 +22,5 @@ via_ir = true
|
||||
[rpc_endpoints]
|
||||
mainnet = "${RPC_URL}"
|
||||
|
||||
|
||||
[fmt]
|
||||
line_length = 80
|
||||
@@ -2,36 +2,42 @@
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
library LibSwap {
|
||||
/// Returns the InToken index into an array of tokens
|
||||
function tokenInIndex(
|
||||
bytes calldata swap
|
||||
) internal pure returns (uint8 res) {
|
||||
res = uint8(swap[0]);
|
||||
/**
|
||||
* @dev Returns arguments required to perform a single swap
|
||||
*/
|
||||
function decodeSingleSwap(bytes calldata swap)
|
||||
internal
|
||||
pure
|
||||
returns (address executor, bytes calldata protocolData)
|
||||
{
|
||||
executor = address(uint160(bytes20(swap[0:20])));
|
||||
protocolData = swap[20:];
|
||||
}
|
||||
|
||||
/// The OutToken index into an array of tokens
|
||||
function tokenOutIndex(
|
||||
bytes calldata swap
|
||||
) internal pure returns (uint8 res) {
|
||||
res = uint8(swap[1]);
|
||||
/**
|
||||
* @dev Returns arguments required to perform a sequential swap
|
||||
*/
|
||||
function decodeSequentialSwap(bytes calldata swap)
|
||||
internal
|
||||
pure
|
||||
returns (address executor, bytes calldata protocolData)
|
||||
{
|
||||
executor = address(uint160(bytes20(swap[0:20])));
|
||||
protocolData = swap[20:];
|
||||
}
|
||||
|
||||
/// The relative amount of token quantity routed into this swap
|
||||
function splitPercentage(
|
||||
bytes calldata swap
|
||||
) internal pure returns (uint24 res) {
|
||||
res = uint24(bytes3(swap[2:5]));
|
||||
}
|
||||
|
||||
/// The address of the executor contract
|
||||
function executor(bytes calldata swap) internal pure returns (address res) {
|
||||
res = address(uint160(bytes20(swap[5:25])));
|
||||
}
|
||||
|
||||
/// Remaining bytes are interpreted as protocol data
|
||||
function protocolData(
|
||||
bytes calldata swap
|
||||
) internal pure returns (bytes calldata res) {
|
||||
res = swap[25:];
|
||||
/**
|
||||
* @dev Returns arguments required to perform a split swap
|
||||
*/
|
||||
function decodeSplitSwap(bytes calldata swap)
|
||||
internal
|
||||
pure
|
||||
returns (uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, address executor, bytes calldata protocolData)
|
||||
{
|
||||
tokenInIndex = uint8(swap[0]);
|
||||
tokenOutIndex = uint8(swap[1]);
|
||||
split = uint24(bytes3(swap[2:5]));
|
||||
executor = address(uint160(bytes20(swap[5:25])));
|
||||
protocolData = swap[25:];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,80 +5,119 @@ const hre = require("hardhat");
|
||||
// Comment out the executors you don't want to deploy
|
||||
const executors_to_deploy = {
|
||||
"ethereum": [
|
||||
// USV2 - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
// USV2 - Args: Factory, Pool Init Code Hash, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
|
||||
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
|
||||
]},
|
||||
// SUSHISWAP - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
|
||||
30
|
||||
]
|
||||
},
|
||||
// SUSHISWAP - Args: Factory, Pool Init Code Hash, Fee BPS, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac",
|
||||
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"
|
||||
]},
|
||||
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
|
||||
30
|
||||
]
|
||||
},
|
||||
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0x1097053Fd2ea711dad45caCcc45EfF7548fCB362",
|
||||
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d"
|
||||
]},
|
||||
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d",
|
||||
25
|
||||
]
|
||||
},
|
||||
// USV3 -Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV3Executor", args: [
|
||||
{
|
||||
exchange: "UniswapV3Executor", args: [
|
||||
"0x1F98431c8aD98523631AE4a59f267346ea31F984",
|
||||
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
|
||||
]},
|
||||
]
|
||||
},
|
||||
// PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash
|
||||
{exchange: "UniswapV3Executor", args: [
|
||||
{
|
||||
exchange: "UniswapV3Executor", args: [
|
||||
"0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9",
|
||||
"0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2"
|
||||
]},
|
||||
]
|
||||
},
|
||||
// Args: Pool manager
|
||||
{exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]},
|
||||
{exchange: "BalancerV2Executor", args: []},
|
||||
// Args: Ekubo core contract
|
||||
{exchange: "EkuboExecutor", args: [
|
||||
{
|
||||
exchange: "EkuboExecutor", args: [
|
||||
"0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444"
|
||||
]}
|
||||
]
|
||||
},
|
||||
// Args: ETH address in curve pools
|
||||
{
|
||||
exchange: "CurveExecutor", args: [
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
// Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
// Args: Factory, Pool Init Code Hash, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6",
|
||||
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
|
||||
]},
|
||||
// SUSHISWAP V2 - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
|
||||
30
|
||||
]
|
||||
},
|
||||
// SUSHISWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0x71524B4f93c58fcbF659783284E38825f0622859",
|
||||
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"
|
||||
]},
|
||||
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
|
||||
30
|
||||
]
|
||||
},
|
||||
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E",
|
||||
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d"
|
||||
]},
|
||||
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d",
|
||||
25
|
||||
]
|
||||
},
|
||||
// USV3 - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV3Executor", args: [
|
||||
{
|
||||
exchange: "UniswapV3Executor", args: [
|
||||
"0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
|
||||
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
|
||||
]},
|
||||
]
|
||||
},
|
||||
// PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash
|
||||
{exchange: "UniswapV3Executor", args: [
|
||||
{
|
||||
exchange: "UniswapV3Executor", args: [
|
||||
"0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9",
|
||||
"0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2"
|
||||
]},
|
||||
]
|
||||
},
|
||||
// Args: Pool manager
|
||||
{exchange: "UniswapV4Executor", args: ["0x498581ff718922c3f8e6a244956af099b2652b2b"]},
|
||||
{exchange: "BalancerV2Executor", args: []},
|
||||
],
|
||||
"unichain": [
|
||||
// Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV2Executor", args: [
|
||||
// Args: Factory, Pool Init Code Hash, Fee BPS
|
||||
{
|
||||
exchange: "UniswapV2Executor", args: [
|
||||
"0x1f98400000000000000000000000000000000002",
|
||||
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
|
||||
]},
|
||||
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
|
||||
30
|
||||
]
|
||||
},
|
||||
// USV3 - Args: Factory, Pool Init Code Hash
|
||||
{exchange: "UniswapV3Executor", args: [
|
||||
{
|
||||
exchange: "UniswapV3Executor", args: [
|
||||
"0x1f98400000000000000000000000000000000003",
|
||||
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
|
||||
]},
|
||||
]
|
||||
},
|
||||
// Args: Pool manager
|
||||
{exchange: "UniswapV4Executor", args: ["0x1f98400000000000000000000000000000000004"]},
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@ pragma solidity ^0.8.26;
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import "@interfaces/ICallback.sol";
|
||||
|
||||
error Dispatcher__UnapprovedExecutor();
|
||||
error Dispatcher__UnapprovedExecutor(address executor);
|
||||
error Dispatcher__NonContractExecutor();
|
||||
error Dispatcher__InvalidDataLength();
|
||||
|
||||
@@ -23,6 +23,10 @@ error Dispatcher__InvalidDataLength();
|
||||
contract Dispatcher {
|
||||
mapping(address => bool) public executors;
|
||||
|
||||
// keccak256("Dispatcher#CURRENTLY_SWAPPING_EXECUTOR_SLOT")
|
||||
uint256 private constant _CURRENTLY_SWAPPING_EXECUTOR_SLOT =
|
||||
0x098a7a3b47801589e8cdf9ec791b93ad44273246946c32ef1fc4dbe45390c80e;
|
||||
|
||||
event ExecutorSet(address indexed executor);
|
||||
event ExecutorRemoved(address indexed executor);
|
||||
|
||||
@@ -52,21 +56,30 @@ contract Dispatcher {
|
||||
* @dev Calls an executor, assumes swap.protocolData contains
|
||||
* protocol-specific data required by the executor.
|
||||
*/
|
||||
// slither-disable-next-line delegatecall-loop
|
||||
function _callExecutor(
|
||||
// slither-disable-next-line delegatecall-loop,assembly
|
||||
function _callSwapOnExecutor(
|
||||
address executor,
|
||||
uint256 amount,
|
||||
bytes calldata data
|
||||
) internal returns (uint256 calculatedAmount) {
|
||||
if (!executors[executor]) {
|
||||
revert Dispatcher__UnapprovedExecutor();
|
||||
revert Dispatcher__UnapprovedExecutor(executor);
|
||||
}
|
||||
|
||||
// slither-disable-next-line controlled-delegatecall,low-level-calls
|
||||
assembly {
|
||||
tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, executor)
|
||||
}
|
||||
|
||||
// slither-disable-next-line controlled-delegatecall,low-level-calls,calls-loop
|
||||
(bool success, bytes memory result) = executor.delegatecall(
|
||||
abi.encodeWithSelector(IExecutor.swap.selector, amount, data)
|
||||
);
|
||||
|
||||
// Clear transient storage in case no callback was performed
|
||||
assembly {
|
||||
tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, 0)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
revert(
|
||||
string(
|
||||
@@ -80,11 +93,18 @@ contract Dispatcher {
|
||||
calculatedAmount = abi.decode(result, (uint256));
|
||||
}
|
||||
|
||||
function _handleCallback(bytes calldata data) internal {
|
||||
address executor = address(uint160(bytes20(data[data.length - 20:])));
|
||||
// slither-disable-next-line assembly
|
||||
function _callHandleCallbackOnExecutor(bytes calldata data)
|
||||
internal
|
||||
returns (bytes memory)
|
||||
{
|
||||
address executor;
|
||||
assembly {
|
||||
executor := tload(_CURRENTLY_SWAPPING_EXECUTOR_SLOT)
|
||||
}
|
||||
|
||||
if (!executors[executor]) {
|
||||
revert Dispatcher__UnapprovedExecutor();
|
||||
revert Dispatcher__UnapprovedExecutor(executor);
|
||||
}
|
||||
|
||||
// slither-disable-next-line controlled-delegatecall,low-level-calls
|
||||
@@ -101,5 +121,14 @@ contract Dispatcher {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// to prevent multiple callbacks
|
||||
assembly {
|
||||
tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, 0)
|
||||
}
|
||||
|
||||
// this is necessary because the delegatecall will prepend extra bytes we don't want like the length and prefix
|
||||
bytes memory decodedResult = abi.decode(result, (bytes));
|
||||
return decodedResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
||||
error TychoRouter__AddressZero();
|
||||
error TychoRouter__EmptySwaps();
|
||||
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
|
||||
error TychoRouter__AmountInDiffersFromConsumed(
|
||||
error TychoRouter__AmountOutNotFullyReceived(
|
||||
uint256 amountIn, uint256 amountConsumed
|
||||
);
|
||||
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
|
||||
@@ -76,8 +76,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
//keccak256("NAME_OF_ROLE") : save gas on deployment
|
||||
bytes32 public constant EXECUTOR_SETTER_ROLE =
|
||||
0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87;
|
||||
bytes32 public constant FEE_SETTER_ROLE =
|
||||
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
|
||||
bytes32 public constant PAUSER_ROLE =
|
||||
0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a;
|
||||
bytes32 public constant UNPAUSER_ROLE =
|
||||
@@ -85,19 +83,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
bytes32 public constant FUND_RESCUER_ROLE =
|
||||
0x912e45d663a6f4cc1d0491d8f046e06c616f40352565ea1cdb86a0e1aaefa41b;
|
||||
|
||||
address public feeReceiver;
|
||||
|
||||
// Fee should be expressed in basis points (1/100th of a percent)
|
||||
// For example, 100 = 1%, 500 = 5%, 1000 = 10%
|
||||
uint256 public fee;
|
||||
|
||||
event Withdrawal(
|
||||
address indexed token, uint256 amount, address indexed receiver
|
||||
);
|
||||
event FeeReceiverSet(
|
||||
address indexed oldFeeReceiver, address indexed newFeeReceiver
|
||||
);
|
||||
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
|
||||
|
||||
constructor(address _permit2, address weth) {
|
||||
if (_permit2 == address(0) || weth == address(0)) {
|
||||
@@ -111,13 +99,12 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
/**
|
||||
* @notice Executes a swap operation based on a predefined swap graph, supporting internal token amount splits.
|
||||
* This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount
|
||||
* against a user-specified minimum. This function performs a transferFrom to retrieve tokens from the caller.
|
||||
* against a user-specified minimum.
|
||||
*
|
||||
* @dev
|
||||
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - Swaps are executed sequentially using the `_swap` function.
|
||||
* - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
@@ -130,9 +117,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param swaps Encoded swap graph data containing details of each swap.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable.
|
||||
* @return amountOut The total amount of the output token received by the receiver.
|
||||
*/
|
||||
function swap(
|
||||
function splitSwap(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
@@ -143,12 +130,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
address receiver,
|
||||
bytes calldata swaps
|
||||
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
if (address(tokenIn) != address(0)) {
|
||||
IERC20(tokenIn).safeTransferFrom(
|
||||
msg.sender, address(this), amountIn
|
||||
);
|
||||
}
|
||||
return _swapChecked(
|
||||
return _splitSwapChecked(
|
||||
amountIn,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
@@ -171,7 +153,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router.
|
||||
* - Swaps are executed sequentially using the `_swap` function.
|
||||
* - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
@@ -186,9 +167,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
* @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
|
||||
* @param swaps Encoded swap graph data containing details of each swap.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable.
|
||||
* @return amountOut The total amount of the output token received by the receiver.
|
||||
*/
|
||||
function swapPermit2(
|
||||
function splitSwapPermit2(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
@@ -201,18 +182,12 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
bytes calldata signature,
|
||||
bytes calldata swaps
|
||||
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
|
||||
// For native ETH, assume funds already in our router. Else, handle approval.
|
||||
if (tokenIn != address(0)) {
|
||||
permit2.permit(msg.sender, permitSingle, signature);
|
||||
permit2.transferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
uint160(amountIn),
|
||||
permitSingle.details.token
|
||||
);
|
||||
}
|
||||
|
||||
return _swapChecked(
|
||||
return _splitSwapChecked(
|
||||
amountIn,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
@@ -226,14 +201,206 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal implementation of the core swap logic shared between swap() and swapPermit2().
|
||||
* @notice Executes a swap operation based on a predefined swap graph with no split routes.
|
||||
* This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount
|
||||
* against a user-specified minimum.
|
||||
*
|
||||
* @dev
|
||||
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - Swaps are executed sequentially using the `_swap` function.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
* @param tokenIn The address of the input token. Use `address(0)` for native ETH
|
||||
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
|
||||
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
|
||||
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
|
||||
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param swaps Encoded swap graph data containing details of each swap.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver.
|
||||
*/
|
||||
function sequentialSwap(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
address receiver,
|
||||
bytes calldata swaps
|
||||
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
return _sequentialSwapChecked(
|
||||
amountIn,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
minAmountOut,
|
||||
wrapEth,
|
||||
unwrapEth,
|
||||
receiver,
|
||||
swaps
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Executes a swap operation based on a predefined swap graph with no split routes.
|
||||
* This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount
|
||||
* against a user-specified minimum.
|
||||
*
|
||||
* @dev
|
||||
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
* @param tokenIn The address of the input token. Use `address(0)` for native ETH
|
||||
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
|
||||
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
|
||||
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
|
||||
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true.
|
||||
* @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
|
||||
* @param swaps Encoded swap graph data containing details of each swap.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver.
|
||||
*/
|
||||
function sequentialSwapPermit2(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
address receiver,
|
||||
IAllowanceTransfer.PermitSingle calldata permitSingle,
|
||||
bytes calldata signature,
|
||||
bytes calldata swaps
|
||||
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
// For native ETH, assume funds already in our router. Else, handle approval.
|
||||
if (tokenIn != address(0)) {
|
||||
permit2.permit(msg.sender, permitSingle, signature);
|
||||
}
|
||||
|
||||
return _sequentialSwapChecked(
|
||||
amountIn,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
minAmountOut,
|
||||
wrapEth,
|
||||
unwrapEth,
|
||||
receiver,
|
||||
swaps
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Executes a single swap operation.
|
||||
* This function enables optional ETH wrapping/unwrapping, and validates the output amount against a user-specified minimum.
|
||||
*
|
||||
* @dev
|
||||
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
* @param tokenIn The address of the input token. Use `address(0)` for native ETH
|
||||
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
|
||||
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
|
||||
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
|
||||
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param swapData Encoded swap details.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver.
|
||||
*/
|
||||
function singleSwap(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
address receiver,
|
||||
bytes calldata swapData
|
||||
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
return _singleSwap(
|
||||
amountIn,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
minAmountOut,
|
||||
wrapEth,
|
||||
unwrapEth,
|
||||
receiver,
|
||||
swapData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Executes a single swap operation.
|
||||
* This function enables optional ETH wrapping/unwrapping, and validates the output amount
|
||||
* against a user-specified minimum.
|
||||
*
|
||||
* @dev
|
||||
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
* @param tokenIn The address of the input token. Use `address(0)` for native ETH
|
||||
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
|
||||
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
|
||||
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
|
||||
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true.
|
||||
* @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
|
||||
* @param swapData Encoded swap details.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver.
|
||||
*/
|
||||
function singleSwapPermit2(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
address receiver,
|
||||
IAllowanceTransfer.PermitSingle calldata permitSingle,
|
||||
bytes calldata signature,
|
||||
bytes calldata swapData
|
||||
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
// For native ETH, assume funds already in our router. Else, handle approval.
|
||||
if (tokenIn != address(0)) {
|
||||
permit2.permit(msg.sender, permitSingle, signature);
|
||||
}
|
||||
|
||||
return _singleSwap(
|
||||
amountIn,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
minAmountOut,
|
||||
wrapEth,
|
||||
unwrapEth,
|
||||
receiver,
|
||||
swapData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal implementation of the core swap logic shared between splitSwap() and splitSwapPermit2().
|
||||
*
|
||||
* @notice This function centralizes the swap execution logic.
|
||||
* @notice For detailed documentation on parameters and behavior, see the documentation for
|
||||
* swap() and swapPermit2() functions.
|
||||
* splitSwap() and splitSwapPermit2() functions.
|
||||
*
|
||||
*/
|
||||
function _swapChecked(
|
||||
function _splitSwapChecked(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
@@ -257,28 +424,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
tokenIn = address(_weth);
|
||||
}
|
||||
|
||||
uint256 initialBalance = tokenIn == address(0)
|
||||
? address(this).balance
|
||||
: IERC20(tokenIn).balanceOf(address(this));
|
||||
|
||||
amountOut = _swap(amountIn, nTokens, swaps);
|
||||
uint256 currentBalance = tokenIn == address(0)
|
||||
? address(this).balance
|
||||
: IERC20(tokenIn).balanceOf(address(this));
|
||||
|
||||
uint256 amountConsumed = initialBalance - currentBalance;
|
||||
|
||||
if (tokenIn != tokenOut && amountConsumed != amountIn) {
|
||||
revert TychoRouter__AmountInDiffersFromConsumed(
|
||||
amountIn, amountConsumed
|
||||
);
|
||||
}
|
||||
|
||||
if (fee > 0) {
|
||||
uint256 feeAmount = (amountOut * fee) / 10000;
|
||||
amountOut -= feeAmount;
|
||||
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
|
||||
}
|
||||
uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
|
||||
amountOut = _splitSwap(amountIn, nTokens, swaps);
|
||||
|
||||
if (amountOut < minAmountOut) {
|
||||
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
|
||||
@@ -286,11 +433,128 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
|
||||
if (unwrapEth) {
|
||||
_unwrapETH(amountOut);
|
||||
}
|
||||
if (tokenOut == address(0)) {
|
||||
Address.sendValue(payable(receiver), amountOut);
|
||||
} else {
|
||||
IERC20(tokenOut).safeTransfer(receiver, amountOut);
|
||||
}
|
||||
|
||||
if (tokenIn != tokenOut) {
|
||||
uint256 currentBalanceTokenOut = _balanceOf(tokenOut, receiver);
|
||||
uint256 userAmount = currentBalanceTokenOut - initialBalanceTokenOut;
|
||||
if (userAmount != amountOut) {
|
||||
revert TychoRouter__AmountOutNotFullyReceived(
|
||||
userAmount, amountOut
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal implementation of the core swap logic shared between singleSwap() and singleSwapPermit2().
|
||||
*
|
||||
* @notice This function centralizes the swap execution logic.
|
||||
* @notice For detailed documentation on parameters and behavior, see the documentation for
|
||||
* singleSwap() and singleSwapPermit2() functions.
|
||||
*
|
||||
*/
|
||||
function _singleSwap(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
address receiver,
|
||||
bytes calldata swap_
|
||||
) internal returns (uint256 amountOut) {
|
||||
if (receiver == address(0)) {
|
||||
revert TychoRouter__AddressZero();
|
||||
}
|
||||
if (minAmountOut == 0) {
|
||||
revert TychoRouter__UndefinedMinAmountOut();
|
||||
}
|
||||
|
||||
// Assume funds are already in the router.
|
||||
if (wrapEth) {
|
||||
_wrapETH(amountIn);
|
||||
tokenIn = address(_weth);
|
||||
}
|
||||
|
||||
(address executor, bytes calldata protocolData) =
|
||||
swap_.decodeSingleSwap();
|
||||
|
||||
uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
|
||||
amountOut = _callSwapOnExecutor(executor, amountIn, protocolData);
|
||||
|
||||
if (amountOut < minAmountOut) {
|
||||
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
|
||||
}
|
||||
|
||||
if (unwrapEth) {
|
||||
_unwrapETH(amountOut);
|
||||
Address.sendValue(payable(receiver), amountOut);
|
||||
}
|
||||
|
||||
if (tokenIn != tokenOut) {
|
||||
uint256 currentBalanceTokenOut = _balanceOf(tokenOut, receiver);
|
||||
uint256 userAmount = currentBalanceTokenOut - initialBalanceTokenOut;
|
||||
if (userAmount != amountOut) {
|
||||
revert TychoRouter__AmountOutNotFullyReceived(
|
||||
userAmount, amountOut
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal implementation of the core swap logic shared between sequentialSwap() and sequentialSwapPermit2().
|
||||
*
|
||||
* @notice This function centralizes the swap execution logic.
|
||||
* @notice For detailed documentation on parameters and behavior, see the documentation for
|
||||
* sequentialSwap() and sequentialSwapPermit2() functions.
|
||||
*
|
||||
*/
|
||||
function _sequentialSwapChecked(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
address receiver,
|
||||
bytes calldata swaps
|
||||
) internal returns (uint256 amountOut) {
|
||||
if (receiver == address(0)) {
|
||||
revert TychoRouter__AddressZero();
|
||||
}
|
||||
if (minAmountOut == 0) {
|
||||
revert TychoRouter__UndefinedMinAmountOut();
|
||||
}
|
||||
|
||||
// Assume funds are already in the router.
|
||||
if (wrapEth) {
|
||||
_wrapETH(amountIn);
|
||||
tokenIn = address(_weth);
|
||||
}
|
||||
|
||||
uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
|
||||
amountOut = _sequentialSwap(amountIn, swaps);
|
||||
|
||||
if (amountOut < minAmountOut) {
|
||||
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
|
||||
}
|
||||
|
||||
if (unwrapEth) {
|
||||
_unwrapETH(amountOut);
|
||||
Address.sendValue(payable(receiver), amountOut);
|
||||
}
|
||||
|
||||
if (tokenIn != tokenOut) {
|
||||
uint256 currentBalanceTokenOut = _balanceOf(tokenOut, receiver);
|
||||
uint256 userAmount = currentBalanceTokenOut - initialBalanceTokenOut;
|
||||
if (userAmount != amountOut) {
|
||||
revert TychoRouter__AmountOutNotFullyReceived(
|
||||
userAmount, amountOut
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,10 +581,11 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
*
|
||||
* @return The total amount of the buy token obtained after all swaps have been executed.
|
||||
*/
|
||||
function _swap(uint256 amountIn, uint256 nTokens, bytes calldata swaps_)
|
||||
internal
|
||||
returns (uint256)
|
||||
{
|
||||
function _splitSwap(
|
||||
uint256 amountIn,
|
||||
uint256 nTokens,
|
||||
bytes calldata swaps_
|
||||
) internal returns (uint256) {
|
||||
if (swaps_.length == 0) {
|
||||
revert TychoRouter__EmptySwaps();
|
||||
}
|
||||
@@ -330,6 +595,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
uint8 tokenInIndex = 0;
|
||||
uint8 tokenOutIndex = 0;
|
||||
uint24 split;
|
||||
address executor;
|
||||
bytes calldata protocolData;
|
||||
bytes calldata swapData;
|
||||
|
||||
uint256[] memory remainingAmounts = new uint256[](nTokens);
|
||||
@@ -340,17 +607,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
|
||||
while (swaps_.length > 0) {
|
||||
(swapData, swaps_) = swaps_.next();
|
||||
tokenInIndex = swapData.tokenInIndex();
|
||||
tokenOutIndex = swapData.tokenOutIndex();
|
||||
split = swapData.splitPercentage();
|
||||
|
||||
(tokenInIndex, tokenOutIndex, split, executor, protocolData) =
|
||||
swapData.decodeSplitSwap();
|
||||
|
||||
currentAmountIn = split > 0
|
||||
? (amounts[tokenInIndex] * split) / 0xffffff
|
||||
: remainingAmounts[tokenInIndex];
|
||||
|
||||
currentAmountOut = _callExecutor(
|
||||
swapData.executor(), currentAmountIn, swapData.protocolData()
|
||||
);
|
||||
currentAmountOut =
|
||||
_callSwapOnExecutor(executor, currentAmountIn, protocolData);
|
||||
// Checks if the output token is the same as the input token
|
||||
if (tokenOutIndex == 0) {
|
||||
cyclicSwapAmountOut += currentAmountOut;
|
||||
@@ -363,11 +629,41 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Executes sequential swaps as defined by the provided swap graph.
|
||||
*
|
||||
* @param amountIn The initial amount of the sell token to be swapped.
|
||||
* @param swaps_ Encoded swap graph data containing the details of each swap operation.
|
||||
*
|
||||
* @return calculatedAmount The total amount of the buy token obtained after all swaps have been executed.
|
||||
*/
|
||||
function _sequentialSwap(uint256 amountIn, bytes calldata swaps_)
|
||||
internal
|
||||
returns (uint256 calculatedAmount)
|
||||
{
|
||||
bytes calldata swap;
|
||||
calculatedAmount = amountIn;
|
||||
while (swaps_.length > 0) {
|
||||
(swap, swaps_) = swaps_.next();
|
||||
|
||||
(address executor, bytes calldata protocolData) =
|
||||
swap.decodeSingleSwap();
|
||||
|
||||
calculatedAmount =
|
||||
_callSwapOnExecutor(executor, calculatedAmount, protocolData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev We use the fallback function to allow flexibility on callback.
|
||||
*/
|
||||
fallback() external {
|
||||
_handleCallback(msg.data);
|
||||
bytes memory result = _callHandleCallbackOnExecutor(msg.data);
|
||||
// slither-disable-next-line assembly
|
||||
assembly ("memory-safe") {
|
||||
// Propagate the calculatedAmount
|
||||
return(add(result, 32), 16)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,26 +716,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
_removeExecutor(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Allows setting the fee receiver.
|
||||
*/
|
||||
function setFeeReceiver(address newfeeReceiver)
|
||||
external
|
||||
onlyRole(FEE_SETTER_ROLE)
|
||||
{
|
||||
if (newfeeReceiver == address(0)) revert TychoRouter__AddressZero();
|
||||
emit FeeReceiverSet(feeReceiver, newfeeReceiver);
|
||||
feeReceiver = newfeeReceiver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Allows setting the fee.
|
||||
*/
|
||||
function setFee(uint256 newFee) external onlyRole(FEE_SETTER_ROLE) {
|
||||
emit FeeSet(fee, newFee);
|
||||
fee = newFee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Allows withdrawing any ERC20 funds if funds get stuck in case of a bug.
|
||||
*/
|
||||
@@ -502,45 +778,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
require(msg.sender.code.length != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Called by UniswapV3 pool when swapping on it.
|
||||
* See in IUniswapV3SwapCallback for documentation.
|
||||
*/
|
||||
function uniswapV3SwapCallback(
|
||||
int256, /* amount0Delta */
|
||||
int256, /* amount1Delta */
|
||||
bytes calldata data
|
||||
) external {
|
||||
if (data.length < 24) revert TychoRouter__InvalidDataLength();
|
||||
// We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data
|
||||
// This way we preserve the bytes calldata (and don't need to convert it to bytes memory)
|
||||
uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset
|
||||
uint256 dataLength =
|
||||
uint256(bytes32(msg.data[dataOffset:dataOffset + 32]));
|
||||
|
||||
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
|
||||
_handleCallback(fullData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Called by PancakeV3 pool when swapping on it.
|
||||
*/
|
||||
function pancakeV3SwapCallback(
|
||||
int256, /* amount0Delta */
|
||||
int256, /* amount1Delta */
|
||||
bytes calldata data
|
||||
) external {
|
||||
if (data.length < 24) revert TychoRouter__InvalidDataLength();
|
||||
// We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data
|
||||
// This way we preserve the bytes calldata (and don't need to convert it to bytes memory)
|
||||
uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset
|
||||
uint256 dataLength =
|
||||
uint256(bytes32(msg.data[dataOffset:dataOffset + 32]));
|
||||
|
||||
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
|
||||
_handleCallback(fullData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Called by UniswapV4 pool manager after achieving unlock state.
|
||||
*/
|
||||
@@ -549,47 +786,19 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
returns (bytes memory)
|
||||
{
|
||||
if (data.length < 24) revert TychoRouter__InvalidDataLength();
|
||||
_handleCallback(data);
|
||||
return "";
|
||||
bytes memory result = _callHandleCallbackOnExecutor(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
function locked(uint256) external {
|
||||
address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D);
|
||||
|
||||
// slither-disable-next-line controlled-delegatecall,low-level-calls
|
||||
(bool success, bytes memory result) = executor.delegatecall(msg.data);
|
||||
|
||||
if (!success) {
|
||||
revert(
|
||||
string(
|
||||
result.length > 0
|
||||
? result
|
||||
: abi.encodePacked("Callback failed")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// slither-disable-next-line assembly
|
||||
assembly ("memory-safe") {
|
||||
// Propagate the swappedAmount
|
||||
return(add(result, 32), 16)
|
||||
}
|
||||
}
|
||||
|
||||
function payCallback(uint256, address /*token*/ ) external {
|
||||
address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D);
|
||||
|
||||
// slither-disable-next-line controlled-delegatecall,low-level-calls
|
||||
(bool success, bytes memory result) = executor.delegatecall(msg.data);
|
||||
|
||||
if (!success) {
|
||||
revert(
|
||||
string(
|
||||
result.length > 0
|
||||
? result
|
||||
: abi.encodePacked("Callback failed")
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @dev Gets balance of a token for a given address. Supports both native ETH and ERC20 tokens.
|
||||
*/
|
||||
function _balanceOf(address token, address owner)
|
||||
internal
|
||||
view
|
||||
returns (uint256)
|
||||
{
|
||||
return
|
||||
token == address(0) ? owner.balance : IERC20(token).balanceOf(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,17 @@ import {
|
||||
import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol";
|
||||
// slither-disable-next-line solc-version
|
||||
import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
|
||||
import {TokenTransfer} from "./TokenTransfer.sol";
|
||||
|
||||
error BalancerV2Executor__InvalidDataLength();
|
||||
|
||||
contract BalancerV2Executor is IExecutor {
|
||||
contract BalancerV2Executor is IExecutor, TokenTransfer {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
|
||||
|
||||
constructor(address _permit2) TokenTransfer(_permit2) {}
|
||||
|
||||
// slither-disable-next-line locked-ether
|
||||
function swap(uint256 givenAmount, bytes calldata data)
|
||||
external
|
||||
@@ -29,12 +32,23 @@ contract BalancerV2Executor is IExecutor {
|
||||
IERC20 tokenOut,
|
||||
bytes32 poolId,
|
||||
address receiver,
|
||||
bool needsApproval
|
||||
bool needsApproval,
|
||||
TransferType transferType
|
||||
) = _decodeData(data);
|
||||
|
||||
_transfer(
|
||||
address(tokenIn),
|
||||
msg.sender,
|
||||
// Receiver can never be the pool, since the pool expects funds in the router contract
|
||||
// Thus, this call will only ever be used to transfer funds from the user into the router.
|
||||
address(this),
|
||||
givenAmount,
|
||||
transferType
|
||||
);
|
||||
|
||||
if (needsApproval) {
|
||||
// slither-disable-next-line unused-return
|
||||
tokenIn.approve(VAULT, type(uint256).max);
|
||||
tokenIn.forceApprove(VAULT, type(uint256).max);
|
||||
}
|
||||
|
||||
IVault.SingleSwap memory singleSwap = IVault.SingleSwap({
|
||||
@@ -67,10 +81,11 @@ contract BalancerV2Executor is IExecutor {
|
||||
IERC20 tokenOut,
|
||||
bytes32 poolId,
|
||||
address receiver,
|
||||
bool needsApproval
|
||||
bool needsApproval,
|
||||
TransferType transferType
|
||||
)
|
||||
{
|
||||
if (data.length != 93) {
|
||||
if (data.length != 94) {
|
||||
revert BalancerV2Executor__InvalidDataLength();
|
||||
}
|
||||
|
||||
@@ -79,5 +94,6 @@ contract BalancerV2Executor is IExecutor {
|
||||
poolId = bytes32(data[40:72]);
|
||||
receiver = address(bytes20(data[72:92]));
|
||||
needsApproval = uint8(data[92]) > 0;
|
||||
transferType = TransferType(uint8(data[93]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ pragma solidity ^0.8.26;
|
||||
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "./TokenTransfer.sol";
|
||||
import "@openzeppelin/contracts/utils/Address.sol";
|
||||
|
||||
error CurveExecutor__AddressZero();
|
||||
error CurveExecutor__InvalidDataLength();
|
||||
|
||||
interface CryptoPool {
|
||||
// slither-disable-next-line naming-convention
|
||||
@@ -32,12 +35,14 @@ interface CryptoPoolETH {
|
||||
// slither-disable-end naming-convention
|
||||
}
|
||||
|
||||
contract CurveExecutor is IExecutor {
|
||||
contract CurveExecutor is IExecutor, TokenTransfer {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable nativeToken;
|
||||
|
||||
constructor(address _nativeToken) {
|
||||
constructor(address _nativeToken, address _permit2)
|
||||
TokenTransfer(_permit2)
|
||||
{
|
||||
if (_nativeToken == address(0)) {
|
||||
revert CurveExecutor__AddressZero();
|
||||
}
|
||||
@@ -50,6 +55,8 @@ contract CurveExecutor is IExecutor {
|
||||
payable
|
||||
returns (uint256)
|
||||
{
|
||||
if (data.length != 85) revert CurveExecutor__InvalidDataLength();
|
||||
|
||||
(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
@@ -57,12 +64,24 @@ contract CurveExecutor is IExecutor {
|
||||
uint8 poolType,
|
||||
int128 i,
|
||||
int128 j,
|
||||
bool tokenApprovalNeeded
|
||||
bool tokenApprovalNeeded,
|
||||
TransferType transferType,
|
||||
address receiver
|
||||
) = _decodeData(data);
|
||||
|
||||
_transfer(
|
||||
tokenIn,
|
||||
msg.sender,
|
||||
// Receiver can never be the pool, since the pool expects funds in the router contract
|
||||
// Thus, this call will only ever be used to transfer funds from the user into the router.
|
||||
address(this),
|
||||
amountIn,
|
||||
transferType
|
||||
);
|
||||
|
||||
if (tokenApprovalNeeded && tokenIn != nativeToken) {
|
||||
// slither-disable-next-line unused-return
|
||||
IERC20(tokenIn).approve(address(pool), type(uint256).max);
|
||||
IERC20(tokenIn).forceApprove(address(pool), type(uint256).max);
|
||||
}
|
||||
|
||||
/// Inspired by Curve's router contract: https://github.com/curvefi/curve-router-ng/blob/9ab006ca848fc7f1995b6fbbecfecc1e0eb29e2a/contracts/Router.vy#L44
|
||||
@@ -92,7 +111,16 @@ contract CurveExecutor is IExecutor {
|
||||
}
|
||||
|
||||
uint256 balanceAfter = _balanceOf(tokenOut);
|
||||
return balanceAfter - balanceBefore;
|
||||
uint256 amountOut = balanceAfter - balanceBefore;
|
||||
|
||||
if (receiver != address(this)) {
|
||||
if (tokenOut == nativeToken) {
|
||||
Address.sendValue(payable(receiver), amountOut);
|
||||
} else {
|
||||
IERC20(tokenOut).safeTransfer(receiver, amountOut);
|
||||
}
|
||||
}
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
function _decodeData(bytes calldata data)
|
||||
@@ -105,7 +133,9 @@ contract CurveExecutor is IExecutor {
|
||||
uint8 poolType,
|
||||
int128 i,
|
||||
int128 j,
|
||||
bool tokenApprovalNeeded
|
||||
bool tokenApprovalNeeded,
|
||||
TransferType transferType,
|
||||
address receiver
|
||||
)
|
||||
{
|
||||
tokenIn = address(bytes20(data[0:20]));
|
||||
@@ -115,6 +145,8 @@ contract CurveExecutor is IExecutor {
|
||||
i = int128(uint128(uint8(data[61])));
|
||||
j = int128(uint128(uint8(data[62])));
|
||||
tokenApprovalNeeded = data[63] != 0;
|
||||
transferType = TransferType(uint8(data[64]));
|
||||
receiver = address(bytes20(data[65:85]));
|
||||
}
|
||||
|
||||
receive() external payable {
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {IExecutor} from "@interfaces/IExecutor.sol";
|
||||
import {ICallback} from "@interfaces/ICallback.sol";
|
||||
import {ICore} from "@ekubo/interfaces/ICore.sol";
|
||||
import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol";
|
||||
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
|
||||
@@ -10,18 +11,28 @@ import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
|
||||
import {LibBytes} from "@solady/utils/LibBytes.sol";
|
||||
import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol";
|
||||
import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol";
|
||||
import {TokenTransfer} from "./TokenTransfer.sol";
|
||||
|
||||
contract EkuboExecutor is IExecutor, ILocker, IPayer {
|
||||
contract EkuboExecutor is
|
||||
IExecutor,
|
||||
ILocker,
|
||||
IPayer,
|
||||
ICallback,
|
||||
TokenTransfer
|
||||
{
|
||||
error EkuboExecutor__InvalidDataLength();
|
||||
error EkuboExecutor__CoreOnly();
|
||||
error EkuboExecutor__UnknownCallback();
|
||||
|
||||
ICore immutable core;
|
||||
|
||||
uint256 constant POOL_DATA_OFFSET = 92;
|
||||
uint256 constant POOL_DATA_OFFSET = 77;
|
||||
uint256 constant HOP_BYTE_LEN = 52;
|
||||
|
||||
constructor(address _core) {
|
||||
bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256)
|
||||
bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address)
|
||||
|
||||
constructor(address _core, address _permit2) TokenTransfer(_permit2) {
|
||||
core = ICore(_core);
|
||||
}
|
||||
|
||||
@@ -30,67 +41,57 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
|
||||
payable
|
||||
returns (uint256 calculatedAmount)
|
||||
{
|
||||
if (data.length < 92) revert EkuboExecutor__InvalidDataLength();
|
||||
if (data.length < 93) revert EkuboExecutor__InvalidDataLength();
|
||||
|
||||
// amountIn must be at most type(int128).MAX
|
||||
calculatedAmount =
|
||||
uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data)));
|
||||
calculatedAmount = uint256(
|
||||
_lock(
|
||||
bytes.concat(
|
||||
bytes16(uint128(amountIn)), bytes20(msg.sender), data
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function handleCallback(bytes calldata raw)
|
||||
external
|
||||
returns (bytes memory)
|
||||
{
|
||||
verifyCallback(raw);
|
||||
|
||||
// Without selector and locker id
|
||||
bytes calldata stripped = raw[36:];
|
||||
|
||||
bytes4 selector = bytes4(raw[:4]);
|
||||
|
||||
bytes memory result = "";
|
||||
if (selector == LOCKED_SELECTOR) {
|
||||
int128 calculatedAmount = _locked(stripped);
|
||||
result = abi.encodePacked(calculatedAmount);
|
||||
} else if (selector == PAY_CALLBACK_SELECTOR) {
|
||||
_payCallback(stripped);
|
||||
} else {
|
||||
revert EkuboExecutor__UnknownCallback();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function verifyCallback(bytes calldata) public view coreOnly {}
|
||||
|
||||
function locked(uint256) external coreOnly {
|
||||
int128 nextAmountIn = int128(uint128(bytes16(msg.data[36:52])));
|
||||
uint128 tokenInDebtAmount = uint128(nextAmountIn);
|
||||
|
||||
address receiver = address(bytes20(msg.data[52:72]));
|
||||
address tokenIn = address(bytes20(msg.data[72:POOL_DATA_OFFSET]));
|
||||
|
||||
address nextTokenIn = tokenIn;
|
||||
|
||||
uint256 hopsLength = (msg.data.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN;
|
||||
|
||||
uint256 offset = POOL_DATA_OFFSET;
|
||||
|
||||
for (uint256 i = 0; i < hopsLength; i++) {
|
||||
address nextTokenOut =
|
||||
address(bytes20(LibBytes.loadCalldata(msg.data, offset)));
|
||||
Config poolConfig =
|
||||
Config.wrap(LibBytes.loadCalldata(msg.data, offset + 20));
|
||||
|
||||
(address token0, address token1, bool isToken1) = nextTokenIn
|
||||
> nextTokenOut
|
||||
? (nextTokenOut, nextTokenIn, true)
|
||||
: (nextTokenIn, nextTokenOut, false);
|
||||
|
||||
// slither-disable-next-line calls-loop
|
||||
(int128 delta0, int128 delta1) = core.swap_611415377(
|
||||
EkuboPoolKey(token0, token1, poolConfig),
|
||||
nextAmountIn,
|
||||
isToken1,
|
||||
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
|
||||
0
|
||||
);
|
||||
|
||||
nextTokenIn = nextTokenOut;
|
||||
nextAmountIn = -(isToken1 ? delta0 : delta1);
|
||||
|
||||
offset += HOP_BYTE_LEN;
|
||||
}
|
||||
|
||||
_pay(tokenIn, tokenInDebtAmount);
|
||||
|
||||
core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
|
||||
|
||||
// Without selector and locker id
|
||||
int128 calculatedAmount = _locked(msg.data[36:]);
|
||||
// slither-disable-next-line assembly
|
||||
assembly ("memory-safe") {
|
||||
mstore(0, nextAmountIn)
|
||||
mstore(0, calculatedAmount)
|
||||
return(0x10, 16)
|
||||
}
|
||||
}
|
||||
|
||||
function payCallback(uint256, address token) external coreOnly {
|
||||
uint128 amount = uint128(bytes16(msg.data[68:84]));
|
||||
|
||||
SafeTransferLib.safeTransfer(token, address(core), amount);
|
||||
function payCallback(uint256, address /*token*/ ) external coreOnly {
|
||||
// Without selector and locker id
|
||||
_payCallback(msg.data[36:]);
|
||||
}
|
||||
|
||||
function _lock(bytes memory data)
|
||||
@@ -121,7 +122,58 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
|
||||
}
|
||||
}
|
||||
|
||||
function _pay(address token, uint128 amount) internal {
|
||||
function _locked(bytes calldata swapData) internal returns (int128) {
|
||||
int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16])));
|
||||
uint128 tokenInDebtAmount = uint128(nextAmountIn);
|
||||
address sender = address(bytes20(swapData[16:36]));
|
||||
uint8 transferType = uint8(swapData[36]);
|
||||
|
||||
address receiver = address(bytes20(swapData[37:57]));
|
||||
address tokenIn = address(bytes20(swapData[57:77]));
|
||||
|
||||
address nextTokenIn = tokenIn;
|
||||
|
||||
uint256 hopsLength = (swapData.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN;
|
||||
|
||||
uint256 offset = POOL_DATA_OFFSET;
|
||||
|
||||
for (uint256 i = 0; i < hopsLength; i++) {
|
||||
address nextTokenOut =
|
||||
address(bytes20(LibBytes.loadCalldata(swapData, offset)));
|
||||
Config poolConfig =
|
||||
Config.wrap(LibBytes.loadCalldata(swapData, offset + 20));
|
||||
|
||||
(address token0, address token1, bool isToken1) = nextTokenIn
|
||||
> nextTokenOut
|
||||
? (nextTokenOut, nextTokenIn, true)
|
||||
: (nextTokenIn, nextTokenOut, false);
|
||||
|
||||
// slither-disable-next-line calls-loop
|
||||
(int128 delta0, int128 delta1) = core.swap_611415377(
|
||||
EkuboPoolKey(token0, token1, poolConfig),
|
||||
nextAmountIn,
|
||||
isToken1,
|
||||
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
|
||||
0
|
||||
);
|
||||
|
||||
nextTokenIn = nextTokenOut;
|
||||
nextAmountIn = -(isToken1 ? delta0 : delta1);
|
||||
|
||||
offset += HOP_BYTE_LEN;
|
||||
}
|
||||
|
||||
_pay(tokenIn, tokenInDebtAmount, sender, transferType);
|
||||
core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
|
||||
return nextAmountIn;
|
||||
}
|
||||
|
||||
function _pay(
|
||||
address token,
|
||||
uint128 amount,
|
||||
address sender,
|
||||
uint8 transferType
|
||||
) internal {
|
||||
address target = address(core);
|
||||
|
||||
if (token == NATIVE_TOKEN_ADDRESS) {
|
||||
@@ -134,9 +186,11 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
|
||||
mstore(free, shl(224, 0x0c11dedd))
|
||||
mstore(add(free, 4), token)
|
||||
mstore(add(free, 36), shl(128, amount))
|
||||
mstore(add(free, 52), shl(96, sender))
|
||||
mstore(add(free, 72), shl(248, transferType))
|
||||
|
||||
// if it failed, pass through revert
|
||||
if iszero(call(gas(), target, 0, free, 52, 0, 0)) {
|
||||
// 4 (selector) + 32 (token) + 16 (amount) + 20 (sender) + 1 (transferType) = 73
|
||||
if iszero(call(gas(), target, 0, free, 73, 0, 0)) {
|
||||
returndatacopy(0, 0, returndatasize())
|
||||
revert(0, returndatasize())
|
||||
}
|
||||
@@ -144,6 +198,14 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
|
||||
}
|
||||
}
|
||||
|
||||
function _payCallback(bytes calldata payData) internal {
|
||||
address token = address(bytes20(payData[12:32])); // This arg is abi-encoded
|
||||
uint128 amount = uint128(bytes16(payData[32:48]));
|
||||
address sender = address(bytes20(payData[48:68]));
|
||||
TransferType transferType = TransferType(uint8(payData[68]));
|
||||
_transfer(token, sender, address(core), amount, transferType);
|
||||
}
|
||||
|
||||
// To receive withdrawals from Core
|
||||
receive() external payable {}
|
||||
|
||||
|
||||
70
foundry/src/executors/TokenTransfer.sol
Normal file
70
foundry/src/executors/TokenTransfer.sol
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
|
||||
|
||||
error TokenTransfer__AddressZero();
|
||||
|
||||
contract TokenTransfer {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
IAllowanceTransfer public immutable permit2;
|
||||
|
||||
enum TransferType {
|
||||
// Assume funds are in the TychoRouter - transfer into the pool
|
||||
TRANSFER_TO_PROTOCOL,
|
||||
// Assume funds are in msg.sender's wallet - transferFrom into the pool
|
||||
TRANSFER_FROM_TO_PROTOCOL,
|
||||
// Assume funds are in msg.sender's wallet - permit2TransferFrom into the pool
|
||||
TRANSFER_PERMIT2_TO_PROTOCOL,
|
||||
// Assume funds are in msg.sender's wallet - but the pool requires it to be
|
||||
// in the router contract when calling swap - transferFrom into the router
|
||||
// contract
|
||||
TRANSFER_FROM_TO_ROUTER,
|
||||
// Assume funds are in msg.sender's wallet - but the pool requires it to be
|
||||
// in the router contract when calling swap - transferFrom into the router
|
||||
// contract using permit2
|
||||
TRANSFER_PERMIT2_TO_ROUTER,
|
||||
// Assume funds have already been transferred into the pool. Do nothing.
|
||||
NONE
|
||||
}
|
||||
|
||||
constructor(address _permit2) {
|
||||
if (_permit2 == address(0)) {
|
||||
revert TokenTransfer__AddressZero();
|
||||
}
|
||||
permit2 = IAllowanceTransfer(_permit2);
|
||||
}
|
||||
|
||||
function _transfer(
|
||||
address tokenIn,
|
||||
address sender,
|
||||
address receiver,
|
||||
uint256 amount,
|
||||
TransferType transferType
|
||||
) internal {
|
||||
if (transferType == TransferType.TRANSFER_TO_PROTOCOL) {
|
||||
if (tokenIn == address(0)) {
|
||||
payable(receiver).transfer(amount);
|
||||
} else {
|
||||
IERC20(tokenIn).safeTransfer(receiver, amount);
|
||||
}
|
||||
} else if (transferType == TransferType.TRANSFER_FROM_TO_PROTOCOL) {
|
||||
// slither-disable-next-line arbitrary-send-erc20
|
||||
IERC20(tokenIn).safeTransferFrom(sender, receiver, amount);
|
||||
} else if (transferType == TransferType.TRANSFER_PERMIT2_TO_PROTOCOL) {
|
||||
// Permit2.permit is already called from the TychoRouter
|
||||
permit2.transferFrom(sender, receiver, uint160(amount), tokenIn);
|
||||
} else if (transferType == TransferType.TRANSFER_FROM_TO_ROUTER) {
|
||||
// slither-disable-next-line arbitrary-send-erc20
|
||||
IERC20(tokenIn).safeTransferFrom(sender, address(this), amount);
|
||||
} else if (transferType == TransferType.TRANSFER_PERMIT2_TO_ROUTER) {
|
||||
// Permit2.permit is already called from the TychoRouter
|
||||
permit2.transferFrom(
|
||||
sender, address(this), uint160(amount), tokenIn
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,28 @@ pragma solidity ^0.8.26;
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol";
|
||||
import "./TokenTransfer.sol";
|
||||
|
||||
error UniswapV2Executor__InvalidDataLength();
|
||||
error UniswapV2Executor__InvalidTarget();
|
||||
error UniswapV2Executor__InvalidFactory();
|
||||
error UniswapV2Executor__InvalidInitCode();
|
||||
error UniswapV2Executor__InvalidFee();
|
||||
|
||||
contract UniswapV2Executor is IExecutor {
|
||||
contract UniswapV2Executor is IExecutor, TokenTransfer {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable factory;
|
||||
bytes32 public immutable initCode;
|
||||
address private immutable self;
|
||||
uint256 public immutable feeBps;
|
||||
|
||||
constructor(address _factory, bytes32 _initCode) {
|
||||
constructor(
|
||||
address _factory,
|
||||
bytes32 _initCode,
|
||||
address _permit2,
|
||||
uint256 _feeBps
|
||||
) TokenTransfer(_permit2) {
|
||||
if (_factory == address(0)) {
|
||||
revert UniswapV2Executor__InvalidFactory();
|
||||
}
|
||||
@@ -26,6 +34,10 @@ contract UniswapV2Executor is IExecutor {
|
||||
}
|
||||
factory = _factory;
|
||||
initCode = _initCode;
|
||||
if (_feeBps > 30) {
|
||||
revert UniswapV2Executor__InvalidFee();
|
||||
}
|
||||
feeBps = _feeBps;
|
||||
self = address(this);
|
||||
}
|
||||
|
||||
@@ -35,17 +47,21 @@ contract UniswapV2Executor is IExecutor {
|
||||
payable
|
||||
returns (uint256 calculatedAmount)
|
||||
{
|
||||
IERC20 tokenIn;
|
||||
address target;
|
||||
address receiver;
|
||||
bool zeroForOne;
|
||||
IERC20 tokenIn;
|
||||
TransferType transferType;
|
||||
|
||||
(tokenIn, target, receiver, zeroForOne) = _decodeData(data);
|
||||
(tokenIn, target, receiver, zeroForOne, transferType) =
|
||||
_decodeData(data);
|
||||
|
||||
_verifyPairAddress(target);
|
||||
|
||||
calculatedAmount = _getAmountOut(target, givenAmount, zeroForOne);
|
||||
tokenIn.safeTransfer(target, givenAmount);
|
||||
_transfer(
|
||||
address(tokenIn), msg.sender, target, givenAmount, transferType
|
||||
);
|
||||
|
||||
IUniswapV2Pair pool = IUniswapV2Pair(target);
|
||||
if (zeroForOne) {
|
||||
@@ -62,16 +78,18 @@ contract UniswapV2Executor is IExecutor {
|
||||
IERC20 inToken,
|
||||
address target,
|
||||
address receiver,
|
||||
bool zeroForOne
|
||||
bool zeroForOne,
|
||||
TransferType transferType
|
||||
)
|
||||
{
|
||||
if (data.length != 61) {
|
||||
if (data.length != 62) {
|
||||
revert UniswapV2Executor__InvalidDataLength();
|
||||
}
|
||||
inToken = IERC20(address(bytes20(data[0:20])));
|
||||
target = address(bytes20(data[20:40]));
|
||||
receiver = address(bytes20(data[40:60]));
|
||||
zeroForOne = uint8(data[60]) > 0;
|
||||
transferType = TransferType(uint8(data[61]));
|
||||
}
|
||||
|
||||
function _getAmountOut(address target, uint256 amountIn, bool zeroForOne)
|
||||
@@ -91,9 +109,9 @@ contract UniswapV2Executor is IExecutor {
|
||||
}
|
||||
|
||||
require(reserveIn > 0 && reserveOut > 0, "L");
|
||||
uint256 amountInWithFee = amountIn * 997;
|
||||
uint256 amountInWithFee = amountIn * (10000 - feeBps);
|
||||
uint256 numerator = amountInWithFee * uint256(reserveOut);
|
||||
uint256 denominator = (uint256(reserveIn) * 1000) + amountInWithFee;
|
||||
uint256 denominator = (uint256(reserveIn) * 10000) + amountInWithFee;
|
||||
amount = numerator / denominator;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ import "@interfaces/IExecutor.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import "@interfaces/ICallback.sol";
|
||||
import {TokenTransfer} from "./TokenTransfer.sol";
|
||||
|
||||
error UniswapV3Executor__InvalidDataLength();
|
||||
error UniswapV3Executor__InvalidFactory();
|
||||
error UniswapV3Executor__InvalidTarget();
|
||||
error UniswapV3Executor__InvalidInitCode();
|
||||
error UniswapV3Executor__InvalidTransferType(uint8 transferType);
|
||||
|
||||
contract UniswapV3Executor is IExecutor, ICallback {
|
||||
contract UniswapV3Executor is IExecutor, ICallback, TokenTransfer {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
uint160 private constant MIN_SQRT_RATIO = 4295128739;
|
||||
@@ -22,7 +24,9 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
bytes32 public immutable initCode;
|
||||
address private immutable self;
|
||||
|
||||
constructor(address _factory, bytes32 _initCode) {
|
||||
constructor(address _factory, bytes32 _initCode, address _permit2)
|
||||
TokenTransfer(_permit2)
|
||||
{
|
||||
if (_factory == address(0)) {
|
||||
revert UniswapV3Executor__InvalidFactory();
|
||||
}
|
||||
@@ -46,7 +50,8 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
bool zeroForOne,
|
||||
TransferType transferType
|
||||
) = _decodeData(data);
|
||||
|
||||
_verifyPairAddress(tokenIn, tokenOut, fee, target);
|
||||
@@ -55,7 +60,8 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
int256 amount1;
|
||||
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||
|
||||
bytes memory callbackData = _makeV3CallbackData(tokenIn, tokenOut, fee);
|
||||
bytes memory callbackData =
|
||||
_makeV3CallbackData(tokenIn, tokenOut, fee, transferType);
|
||||
|
||||
{
|
||||
(amount0, amount1) = pool.swap(
|
||||
@@ -80,6 +86,7 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
returns (bytes memory result)
|
||||
{
|
||||
// The data has the following layout:
|
||||
// - selector (4 bytes)
|
||||
// - amount0Delta (32 bytes)
|
||||
// - amount1Delta (32 bytes)
|
||||
// - dataOffset (32 bytes)
|
||||
@@ -87,16 +94,25 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
// - protocolData (variable length)
|
||||
|
||||
(int256 amount0Delta, int256 amount1Delta) =
|
||||
abi.decode(msgData[:64], (int256, int256));
|
||||
abi.decode(msgData[4:68], (int256, int256));
|
||||
|
||||
address tokenIn = address(bytes20(msgData[128:148]));
|
||||
address tokenIn = address(bytes20(msgData[132:152]));
|
||||
|
||||
verifyCallback(msgData[128:]);
|
||||
// Transfer type does not exist
|
||||
if (uint8(msgData[175]) > uint8(TransferType.NONE)) {
|
||||
revert UniswapV3Executor__InvalidTransferType(uint8(msgData[175]));
|
||||
}
|
||||
|
||||
TransferType transferType = TransferType(uint8(msgData[175]));
|
||||
address sender = address(bytes20(msgData[176:196]));
|
||||
|
||||
verifyCallback(msgData[132:]);
|
||||
|
||||
uint256 amountOwed =
|
||||
amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
|
||||
|
||||
IERC20(tokenIn).safeTransfer(msg.sender, amountOwed);
|
||||
_transfer(tokenIn, sender, msg.sender, amountOwed, transferType);
|
||||
|
||||
return abi.encode(amountOwed, tokenIn);
|
||||
}
|
||||
|
||||
@@ -113,13 +129,7 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
int256, /* amount1Delta */
|
||||
bytes calldata /* data */
|
||||
) external {
|
||||
uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset
|
||||
uint256 dataLength =
|
||||
uint256(bytes32(msg.data[dataOffset:dataOffset + 32]));
|
||||
|
||||
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
|
||||
|
||||
handleCallback(fullData);
|
||||
handleCallback(msg.data);
|
||||
}
|
||||
|
||||
function _decodeData(bytes calldata data)
|
||||
@@ -131,10 +141,11 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
bool zeroForOne,
|
||||
TransferType transferType
|
||||
)
|
||||
{
|
||||
if (data.length != 84) {
|
||||
if (data.length != 85) {
|
||||
revert UniswapV3Executor__InvalidDataLength();
|
||||
}
|
||||
tokenIn = address(bytes20(data[0:20]));
|
||||
@@ -143,14 +154,18 @@ contract UniswapV3Executor is IExecutor, ICallback {
|
||||
receiver = address(bytes20(data[43:63]));
|
||||
target = address(bytes20(data[63:83]));
|
||||
zeroForOne = uint8(data[83]) > 0;
|
||||
transferType = TransferType(uint8(data[84]));
|
||||
}
|
||||
|
||||
function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee)
|
||||
internal
|
||||
view
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodePacked(tokenIn, tokenOut, fee, self);
|
||||
function _makeV3CallbackData(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
TransferType transferType
|
||||
) internal view returns (bytes memory) {
|
||||
return abi.encodePacked(
|
||||
tokenIn, tokenOut, fee, uint8(transferType), msg.sender
|
||||
);
|
||||
}
|
||||
|
||||
function _verifyPairAddress(
|
||||
|
||||
@@ -2,27 +2,53 @@
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import {ICallback} from "@interfaces/ICallback.sol";
|
||||
import {TokenTransfer} from "./TokenTransfer.sol";
|
||||
import {
|
||||
IERC20,
|
||||
SafeERC20
|
||||
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
||||
import {
|
||||
Currency, CurrencyLibrary
|
||||
} from "@uniswap/v4-core/src/types/Currency.sol";
|
||||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
||||
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
||||
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
||||
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
||||
import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol";
|
||||
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
|
||||
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
|
||||
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
|
||||
import {ICallback} from "@interfaces/ICallback.sol";
|
||||
import {IUnlockCallback} from
|
||||
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
|
||||
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
|
||||
import {TransientStateLibrary} from
|
||||
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
|
||||
|
||||
error UniswapV4Executor__InvalidDataLength();
|
||||
error UniswapV4Executor__NotPoolManager();
|
||||
error UniswapV4Executor__UnknownCallback(bytes4 selector);
|
||||
error UniswapV4Executor__DeltaNotPositive(Currency currency);
|
||||
error UniswapV4Executor__DeltaNotNegative(Currency currency);
|
||||
error UniswapV4Executor__V4TooMuchRequested(
|
||||
uint256 maxAmountInRequested, uint256 amountRequested
|
||||
);
|
||||
|
||||
contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
contract UniswapV4Executor is
|
||||
IExecutor,
|
||||
IUnlockCallback,
|
||||
ICallback,
|
||||
TokenTransfer
|
||||
{
|
||||
using SafeERC20 for IERC20;
|
||||
using CurrencyLibrary for Currency;
|
||||
using SafeCast for *;
|
||||
using TransientStateLibrary for IPoolManager;
|
||||
|
||||
IPoolManager public immutable poolManager;
|
||||
address private immutable _self;
|
||||
|
||||
bytes4 constant SWAP_EXACT_INPUT_SINGLE_SELECTOR = 0x8bc6d0d7;
|
||||
bytes4 constant SWAP_EXACT_INPUT_SELECTOR = 0xaf90aeb1;
|
||||
|
||||
struct UniswapV4Pool {
|
||||
address intermediaryToken;
|
||||
@@ -30,7 +56,22 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
int24 tickSpacing;
|
||||
}
|
||||
|
||||
constructor(IPoolManager _poolManager) V4Router(_poolManager) {}
|
||||
constructor(IPoolManager _poolManager, address _permit2)
|
||||
TokenTransfer(_permit2)
|
||||
{
|
||||
poolManager = _poolManager;
|
||||
_self = address(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Modifier to restrict access to only the pool manager.
|
||||
*/
|
||||
modifier poolManagerOnly() virtual {
|
||||
if (msg.sender != address(poolManager)) {
|
||||
revert UniswapV4Executor__NotPoolManager();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
function swap(uint256 amountIn, bytes calldata data)
|
||||
external
|
||||
@@ -41,10 +82,10 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
bool zeroForOne,
|
||||
address callbackExecutor,
|
||||
TransferType transferType,
|
||||
address receiver,
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools
|
||||
) = _decodeData(data);
|
||||
|
||||
bytes memory swapData;
|
||||
if (pools.length == 1) {
|
||||
PoolKey memory key = PoolKey({
|
||||
@@ -54,26 +95,16 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
tickSpacing: pools[0].tickSpacing,
|
||||
hooks: IHooks(address(0))
|
||||
});
|
||||
bytes memory actions = abi.encodePacked(
|
||||
uint8(Actions.SWAP_EXACT_IN_SINGLE),
|
||||
uint8(Actions.SETTLE_ALL),
|
||||
uint8(Actions.TAKE_ALL)
|
||||
swapData = abi.encodeWithSelector(
|
||||
this.swapExactInputSingle.selector,
|
||||
key,
|
||||
zeroForOne,
|
||||
amountIn,
|
||||
msg.sender,
|
||||
transferType,
|
||||
receiver,
|
||||
bytes("")
|
||||
);
|
||||
|
||||
bytes[] memory params = new bytes[](3);
|
||||
|
||||
params[0] = abi.encode(
|
||||
IV4Router.ExactInputSingleParams({
|
||||
poolKey: key,
|
||||
zeroForOne: zeroForOne,
|
||||
amountIn: uint128(amountIn),
|
||||
amountOutMinimum: uint128(0),
|
||||
hookData: bytes("")
|
||||
})
|
||||
);
|
||||
params[1] = abi.encode(tokenIn, amountIn); // currency to settle
|
||||
params[2] = abi.encode(tokenOut, uint256(0)); // currency to take
|
||||
swapData = abi.encode(actions, params);
|
||||
} else {
|
||||
PathKey[] memory path = new PathKey[](pools.length);
|
||||
for (uint256 i = 0; i < pools.length; i++) {
|
||||
@@ -86,51 +117,22 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
});
|
||||
}
|
||||
|
||||
bytes memory actions = abi.encodePacked(
|
||||
uint8(Actions.SWAP_EXACT_IN),
|
||||
uint8(Actions.SETTLE_ALL),
|
||||
uint8(Actions.TAKE_ALL)
|
||||
);
|
||||
|
||||
bytes[] memory params = new bytes[](3);
|
||||
|
||||
Currency currencyIn = Currency.wrap(tokenIn);
|
||||
params[0] = abi.encode(
|
||||
IV4Router.ExactInputParams({
|
||||
currencyIn: currencyIn,
|
||||
path: path,
|
||||
amountIn: uint128(amountIn),
|
||||
amountOutMinimum: uint128(0)
|
||||
})
|
||||
swapData = abi.encodeWithSelector(
|
||||
this.swapExactInput.selector,
|
||||
currencyIn,
|
||||
path,
|
||||
amountIn,
|
||||
msg.sender,
|
||||
transferType,
|
||||
receiver
|
||||
);
|
||||
params[1] = abi.encode(currencyIn, amountIn);
|
||||
params[2] = abi.encode(Currency.wrap(tokenOut), uint256(0));
|
||||
swapData = abi.encode(actions, params);
|
||||
}
|
||||
bytes memory fullData = abi.encodePacked(swapData, callbackExecutor);
|
||||
uint256 tokenOutBalanceBefore;
|
||||
|
||||
tokenOutBalanceBefore = tokenOut == address(0)
|
||||
? address(this).balance
|
||||
: IERC20(tokenOut).balanceOf(address(this));
|
||||
|
||||
executeActions(fullData);
|
||||
|
||||
uint256 tokenOutBalanceAfter;
|
||||
|
||||
tokenOutBalanceAfter = tokenOut == address(0)
|
||||
? address(this).balance
|
||||
: IERC20(tokenOut).balanceOf(address(this));
|
||||
|
||||
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
|
||||
|
||||
return calculatedAmount;
|
||||
}
|
||||
|
||||
// necessary to convert bytes memory to bytes calldata
|
||||
function executeActions(bytes memory unlockData) public {
|
||||
// slither-disable-next-line unused-return
|
||||
poolManager.unlock(unlockData);
|
||||
bytes memory result = poolManager.unlock(swapData);
|
||||
uint128 amountOut = abi.decode(result, (uint128));
|
||||
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
function _decodeData(bytes calldata data)
|
||||
@@ -140,22 +142,24 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
bool zeroForOne,
|
||||
address callbackExecutor,
|
||||
TransferType transferType,
|
||||
address receiver,
|
||||
UniswapV4Pool[] memory pools
|
||||
)
|
||||
{
|
||||
if (data.length < 87) {
|
||||
if (data.length < 88) {
|
||||
revert UniswapV4Executor__InvalidDataLength();
|
||||
}
|
||||
|
||||
tokenIn = address(bytes20(data[0:20]));
|
||||
tokenOut = address(bytes20(data[20:40]));
|
||||
zeroForOne = (data[40] != 0);
|
||||
callbackExecutor = address(bytes20(data[41:61]));
|
||||
transferType = TransferType(uint8(data[41]));
|
||||
receiver = address(bytes20(data[42:62]));
|
||||
|
||||
uint256 poolsLength = (data.length - 61) / 26; // 26 bytes per pool object
|
||||
uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object
|
||||
pools = new UniswapV4Pool[](poolsLength);
|
||||
bytes memory poolsData = data[61:];
|
||||
bytes memory poolsData = data[62:];
|
||||
uint256 offset = 0;
|
||||
for (uint256 i = 0; i < poolsLength; i++) {
|
||||
address intermediaryToken;
|
||||
@@ -173,6 +177,9 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Handles the callback from the pool manager. This is used for callbacks from the router.
|
||||
*/
|
||||
function handleCallback(bytes calldata data)
|
||||
external
|
||||
returns (bytes memory)
|
||||
@@ -181,15 +188,253 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
|
||||
return _unlockCallback(data);
|
||||
}
|
||||
|
||||
function verifyCallback(bytes calldata) public view onlyPoolManager {}
|
||||
function verifyCallback(bytes calldata) public view poolManagerOnly {}
|
||||
|
||||
function _pay(Currency token, address, uint256 amount) internal override {
|
||||
IERC20(Currency.unwrap(token)).safeTransfer(
|
||||
address(poolManager), amount
|
||||
/**
|
||||
* @notice Handles the unlock callback from the pool manager. This is used for swaps against the executor directly (bypassing the router).
|
||||
*/
|
||||
function unlockCallback(bytes calldata data)
|
||||
external
|
||||
poolManagerOnly
|
||||
returns (bytes memory)
|
||||
{
|
||||
return _unlockCallback(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to handle the unlock callback.
|
||||
*/
|
||||
function _unlockCallback(bytes calldata data)
|
||||
internal
|
||||
returns (bytes memory)
|
||||
{
|
||||
bytes4 selector = bytes4(data[:4]);
|
||||
if (
|
||||
selector != SWAP_EXACT_INPUT_SELECTOR
|
||||
&& selector != SWAP_EXACT_INPUT_SINGLE_SELECTOR
|
||||
) {
|
||||
revert UniswapV4Executor__UnknownCallback(selector);
|
||||
}
|
||||
|
||||
// here we expect to call either `swapExactInputSingle` or `swapExactInput`. See `swap` to see how we encode the selector and the calldata
|
||||
// slither-disable-next-line low-level-calls
|
||||
(bool success, bytes memory returnData) = _self.delegatecall(data);
|
||||
if (!success) {
|
||||
revert(
|
||||
string(
|
||||
returnData.length > 0
|
||||
? returnData
|
||||
: abi.encodePacked("Uniswap v4 Callback failed")
|
||||
)
|
||||
);
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
function msgSender() public view override returns (address) {
|
||||
return address(this);
|
||||
/**
|
||||
* @notice Performs an exact input single swap. It settles and takes the tokens after the swap.
|
||||
* @param poolKey The key of the pool to swap in.
|
||||
* @param zeroForOne Whether the swap is from token0 to token1 (true) or vice versa (false).
|
||||
* @param amountIn The amount of tokens to swap in.
|
||||
* @param sender The address of the sender.
|
||||
* @param transferType The type of transfer in to use.
|
||||
* @param receiver The address of the receiver.
|
||||
* @param hookData Additional data for hook contracts.
|
||||
*/
|
||||
function swapExactInputSingle(
|
||||
PoolKey memory poolKey,
|
||||
bool zeroForOne,
|
||||
uint128 amountIn,
|
||||
address sender,
|
||||
TransferType transferType,
|
||||
address receiver,
|
||||
bytes calldata hookData
|
||||
) external returns (uint128) {
|
||||
uint128 amountOut = _swap(
|
||||
poolKey, zeroForOne, -int256(uint256(amountIn)), hookData
|
||||
).toUint128();
|
||||
|
||||
Currency currencyIn = zeroForOne ? poolKey.currency0 : poolKey.currency1;
|
||||
uint256 amount = _getFullDebt(currencyIn);
|
||||
if (amount > amountIn) {
|
||||
revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
|
||||
}
|
||||
_settle(currencyIn, amount, sender, transferType);
|
||||
|
||||
Currency currencyOut =
|
||||
zeroForOne ? poolKey.currency1 : poolKey.currency0;
|
||||
_take(currencyOut, receiver, _mapTakeAmount(amountOut, currencyOut));
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Performs an exact input swap along a path. It settles and takes the tokens after the swap.
|
||||
* @param currencyIn The currency of the input token.
|
||||
* @param path The path to swap along.
|
||||
* @param amountIn The amount of tokens to swap in.
|
||||
* @param sender The address of the sender.
|
||||
* @param transferType The type of transfer in to use.
|
||||
* @param receiver The address of the receiver.
|
||||
*/
|
||||
function swapExactInput(
|
||||
Currency currencyIn,
|
||||
PathKey[] calldata path,
|
||||
uint128 amountIn,
|
||||
address sender,
|
||||
TransferType transferType,
|
||||
address receiver
|
||||
) external returns (uint128) {
|
||||
uint128 amountOut = 0;
|
||||
Currency swapCurrencyIn = currencyIn;
|
||||
uint256 swapAmountIn = amountIn;
|
||||
unchecked {
|
||||
uint256 pathLength = path.length;
|
||||
PathKey calldata pathKey;
|
||||
|
||||
for (uint256 i = 0; i < pathLength; i++) {
|
||||
pathKey = path[i];
|
||||
(PoolKey memory poolKey, bool zeroForOne) =
|
||||
pathKey.getPoolAndSwapDirection(swapCurrencyIn);
|
||||
// The output delta will always be positive, except for when interacting with certain hook pools
|
||||
amountOut = _swap(
|
||||
poolKey,
|
||||
zeroForOne,
|
||||
-int256(uint256(swapAmountIn)),
|
||||
pathKey.hookData
|
||||
).toUint128();
|
||||
|
||||
swapAmountIn = amountOut;
|
||||
swapCurrencyIn = pathKey.intermediateCurrency;
|
||||
}
|
||||
}
|
||||
|
||||
uint256 amount = _getFullDebt(currencyIn);
|
||||
if (amount > amountIn) {
|
||||
revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
|
||||
}
|
||||
_settle(currencyIn, amount, sender, transferType);
|
||||
|
||||
_take(
|
||||
swapCurrencyIn, // at the end of the loop this is actually currency out
|
||||
receiver,
|
||||
_mapTakeAmount(amountOut, swapCurrencyIn)
|
||||
);
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
function _swap(
|
||||
PoolKey memory poolKey,
|
||||
bool zeroForOne,
|
||||
int256 amountSpecified,
|
||||
bytes calldata hookData
|
||||
) private returns (int128 reciprocalAmount) {
|
||||
unchecked {
|
||||
// slither-disable-next-line calls-loop
|
||||
BalanceDelta delta = poolManager.swap(
|
||||
poolKey,
|
||||
IPoolManager.SwapParams(
|
||||
zeroForOne,
|
||||
amountSpecified,
|
||||
zeroForOne
|
||||
? TickMath.MIN_SQRT_PRICE + 1
|
||||
: TickMath.MAX_SQRT_PRICE - 1
|
||||
),
|
||||
hookData
|
||||
);
|
||||
|
||||
reciprocalAmount = (zeroForOne == amountSpecified < 0)
|
||||
? delta.amount1()
|
||||
: delta.amount0();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Obtains the full amount owed by this contract (negative delta).
|
||||
* @param currency The currency to get the delta for.
|
||||
* @return amount The amount owed by this contract.
|
||||
*/
|
||||
function _getFullCredit(Currency currency)
|
||||
internal
|
||||
view
|
||||
returns (uint256 amount)
|
||||
{
|
||||
int256 _amount = poolManager.currencyDelta(address(this), currency);
|
||||
// If the amount is negative, it should be settled not taken.
|
||||
if (_amount < 0) revert UniswapV4Executor__DeltaNotPositive(currency);
|
||||
amount = uint256(_amount);
|
||||
}
|
||||
|
||||
/// @notice Obtain the full amount owed by this contract (negative delta)
|
||||
/// @param currency Currency to get the delta for
|
||||
/// @return amount The amount owed by this contract as a uint256
|
||||
function _getFullDebt(Currency currency)
|
||||
internal
|
||||
view
|
||||
returns (uint256 amount)
|
||||
{
|
||||
int256 _amount = poolManager.currencyDelta(address(this), currency);
|
||||
// If the amount is positive, it should be taken not settled.
|
||||
if (_amount > 0) revert UniswapV4Executor__DeltaNotNegative(currency);
|
||||
// Casting is safe due to limits on the total supply of a pool
|
||||
amount = uint256(-_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Pays and settles a currency to the pool manager.
|
||||
* @dev The implementing contract must ensure that the `payer` is a secure address.
|
||||
* @param currency The currency to settle.
|
||||
* @param amount The amount to send.
|
||||
* @param sender The address of the payer.
|
||||
* @param transferType The type of transfer to use.
|
||||
* @dev Returns early if the amount is 0.
|
||||
*/
|
||||
function _settle(
|
||||
Currency currency,
|
||||
uint256 amount,
|
||||
address sender,
|
||||
TransferType transferType
|
||||
) internal {
|
||||
if (amount == 0) return;
|
||||
poolManager.sync(currency);
|
||||
if (currency.isAddressZero()) {
|
||||
// slither-disable-next-line unused-return
|
||||
poolManager.settle{value: amount}();
|
||||
} else {
|
||||
_transfer(
|
||||
Currency.unwrap(currency),
|
||||
sender,
|
||||
address(poolManager),
|
||||
amount,
|
||||
transferType
|
||||
);
|
||||
// slither-disable-next-line unused-return
|
||||
poolManager.settle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Takes an amount of currency out of the pool manager.
|
||||
* @param currency The currency to take.
|
||||
* @param recipient The address to receive the currency.
|
||||
* @param amount The amount to take.
|
||||
* @dev Returns early if the amount is 0.
|
||||
*/
|
||||
function _take(Currency currency, address recipient, uint256 amount)
|
||||
internal
|
||||
{
|
||||
if (amount == 0) return;
|
||||
poolManager.take(currency, recipient, amount);
|
||||
}
|
||||
|
||||
function _mapTakeAmount(uint256 amount, Currency currency)
|
||||
internal
|
||||
view
|
||||
returns (uint256)
|
||||
{
|
||||
if (amount == 0) {
|
||||
return _getFullCredit(currency);
|
||||
} else {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ contract Constants is Test, BaseConstants {
|
||||
address ADMIN = makeAddr("admin"); //admin=us
|
||||
address BOB = makeAddr("bob"); //bob=someone!=us
|
||||
address FUND_RESCUER = makeAddr("fundRescuer");
|
||||
address FEE_SETTER = makeAddr("feeSetter");
|
||||
address FEE_RECEIVER = makeAddr("feeReceiver");
|
||||
address EXECUTOR_SETTER = makeAddr("executorSetter");
|
||||
address ALICE = 0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2;
|
||||
uint256 ALICE_PK =
|
||||
|
||||
@@ -10,7 +10,7 @@ contract DispatcherExposed is Dispatcher {
|
||||
uint256 amount,
|
||||
bytes calldata data
|
||||
) external returns (uint256 calculatedAmount) {
|
||||
return _callExecutor(executor, amount, data);
|
||||
return _callSwapOnExecutor(executor, amount, data);
|
||||
}
|
||||
|
||||
function exposedSetExecutor(address target) external {
|
||||
|
||||
@@ -7,7 +7,45 @@ import "../lib/LibSwap.sol";
|
||||
contract LibSwapTest is Test {
|
||||
using LibSwap for bytes;
|
||||
|
||||
function testSwap() public view {
|
||||
function testSingleSwap() public view {
|
||||
address executor = 0x1234567890123456789012345678901234567890;
|
||||
bytes memory protocolData = abi.encodePacked(uint256(123));
|
||||
|
||||
bytes memory swap = abi.encodePacked(executor, protocolData);
|
||||
this.assertSingleSwap(swap, executor, protocolData);
|
||||
}
|
||||
|
||||
function assertSingleSwap(
|
||||
bytes calldata swap,
|
||||
address executor,
|
||||
bytes calldata protocolData
|
||||
) public pure {
|
||||
(address decodedExecutor, bytes memory decodedProtocolData) =
|
||||
swap.decodeSingleSwap();
|
||||
assertEq(decodedExecutor, executor);
|
||||
assertEq(decodedProtocolData, protocolData);
|
||||
}
|
||||
|
||||
function testSequentialSwap() public view {
|
||||
address executor = 0x1234567890123456789012345678901234567890;
|
||||
bytes memory protocolData = abi.encodePacked(uint256(234));
|
||||
|
||||
bytes memory swap = abi.encodePacked(executor, protocolData);
|
||||
this.assertSequentialSwap(swap, executor, protocolData);
|
||||
}
|
||||
|
||||
function assertSequentialSwap(
|
||||
bytes calldata swap,
|
||||
address executor,
|
||||
bytes calldata protocolData
|
||||
) public pure {
|
||||
(address decodedExecutor, bytes memory decodedProtocolData) =
|
||||
swap.decodeSequentialSwap();
|
||||
assertEq(decodedExecutor, executor);
|
||||
assertEq(decodedProtocolData, protocolData);
|
||||
}
|
||||
|
||||
function testSplitSwap() public view {
|
||||
uint8 tokenInIndex = 1;
|
||||
uint8 tokenOutIndex = 2;
|
||||
uint24 split = 3;
|
||||
@@ -17,20 +55,32 @@ contract LibSwapTest is Test {
|
||||
bytes memory swap = abi.encodePacked(
|
||||
tokenInIndex, tokenOutIndex, split, executor, protocolData
|
||||
);
|
||||
this.assertSwap(swap, tokenInIndex, tokenOutIndex, split, executor);
|
||||
this.assertSplitSwap(
|
||||
swap, tokenInIndex, tokenOutIndex, split, executor, protocolData
|
||||
);
|
||||
}
|
||||
|
||||
// This is necessary so that the compiler accepts bytes as a LibSwap.sol
|
||||
function assertSwap(
|
||||
// This is necessary so that the compiler accepts bytes as a LibSwap.sol for testing
|
||||
// This is because this function takes calldata as input
|
||||
function assertSplitSwap(
|
||||
bytes calldata swap,
|
||||
uint8 tokenInIndex,
|
||||
uint8 tokenOutIndex,
|
||||
uint24 split,
|
||||
address executor
|
||||
address executor,
|
||||
bytes calldata protocolData
|
||||
) public pure {
|
||||
assert(swap.tokenInIndex() == tokenInIndex);
|
||||
assert(swap.tokenOutIndex() == tokenOutIndex);
|
||||
assert(swap.splitPercentage() == split);
|
||||
assert(swap.executor() == executor);
|
||||
(
|
||||
uint8 decodedTokenInIndex,
|
||||
uint8 decodedTokenOutIndex,
|
||||
uint24 decodedSplit,
|
||||
address decodedExecutor,
|
||||
bytes memory decodedProtocolData
|
||||
) = swap.decodeSplitSwap();
|
||||
assertEq(decodedTokenInIndex, tokenInIndex);
|
||||
assertEq(decodedTokenOutIndex, tokenOutIndex);
|
||||
assertEq(decodedSplit, split);
|
||||
assertEq(decodedExecutor, executor);
|
||||
assertEq(decodedProtocolData, protocolData);
|
||||
}
|
||||
}
|
||||
|
||||
87
foundry/test/Permit2TestHelper.sol
Normal file
87
foundry/test/Permit2TestHelper.sol
Normal file
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./Constants.sol";
|
||||
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
contract Permit2TestHelper is Constants {
|
||||
/**
|
||||
* @dev Handles the Permit2 approval process for Alice, allowing the TychoRouter contract
|
||||
* to spend `amount_in` of `tokenIn` on her behalf.
|
||||
*
|
||||
* This function approves the Permit2 contract to transfer the specified token amount
|
||||
* and constructs a `PermitSingle` struct for the approval. It also generates a valid
|
||||
* EIP-712 signature for the approval using Alice's private key.
|
||||
*
|
||||
* @param tokenIn The address of the token being approved.
|
||||
* @param amount_in The amount of tokens to approve for transfer.
|
||||
* @return permitSingle The `PermitSingle` struct containing the approval details.
|
||||
* @return signature The EIP-712 signature for the approval.
|
||||
*/
|
||||
function handlePermit2Approval(
|
||||
address tokenIn,
|
||||
address spender,
|
||||
uint256 amount_in
|
||||
) internal returns (IAllowanceTransfer.PermitSingle memory, bytes memory) {
|
||||
IERC20(tokenIn).approve(PERMIT2_ADDRESS, amount_in);
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer
|
||||
.PermitSingle({
|
||||
details: IAllowanceTransfer.PermitDetails({
|
||||
token: tokenIn,
|
||||
amount: uint160(amount_in),
|
||||
expiration: uint48(block.timestamp + 1 days),
|
||||
nonce: 0
|
||||
}),
|
||||
spender: spender,
|
||||
sigDeadline: block.timestamp + 1 days
|
||||
});
|
||||
|
||||
bytes memory signature = signPermit2(permitSingle, ALICE_PK);
|
||||
return (permitSingle, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Signs a Permit2 `PermitSingle` struct with the given private key.
|
||||
* @param permit The `PermitSingle` struct to sign.
|
||||
* @param privateKey The private key of the signer.
|
||||
* @return The signature as a `bytes` array.
|
||||
*/
|
||||
function signPermit2(
|
||||
IAllowanceTransfer.PermitSingle memory permit,
|
||||
uint256 privateKey
|
||||
) internal view returns (bytes memory) {
|
||||
bytes32 _PERMIT_DETAILS_TYPEHASH = keccak256(
|
||||
"PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||
);
|
||||
bytes32 _PERMIT_SINGLE_TYPEHASH = keccak256(
|
||||
"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||
);
|
||||
bytes32 domainSeparator = keccak256(
|
||||
abi.encode(
|
||||
keccak256(
|
||||
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
|
||||
),
|
||||
keccak256("Permit2"),
|
||||
block.chainid,
|
||||
PERMIT2_ADDRESS
|
||||
)
|
||||
);
|
||||
bytes32 detailsHash =
|
||||
keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, permit.details));
|
||||
bytes32 permitHash = keccak256(
|
||||
abi.encode(
|
||||
_PERMIT_SINGLE_TYPEHASH,
|
||||
detailsHash,
|
||||
permit.spender,
|
||||
permit.sigDeadline
|
||||
)
|
||||
);
|
||||
|
||||
bytes32 digest =
|
||||
keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitHash));
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
|
||||
|
||||
return abi.encodePacked(r, s, v);
|
||||
}
|
||||
}
|
||||
29
foundry/test/TestUtils.sol
Normal file
29
foundry/test/TestUtils.sol
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.10;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
contract TestUtils is Test {
|
||||
constructor() {}
|
||||
|
||||
function loadCallDataFromFile(string memory testName)
|
||||
internal
|
||||
view
|
||||
returns (bytes memory)
|
||||
{
|
||||
string memory fileContent = vm.readFile("./test/assets/calldata.txt");
|
||||
string[] memory lines = vm.split(fileContent, "\n");
|
||||
|
||||
for (uint256 i = 0; i < lines.length; i++) {
|
||||
string[] memory parts = vm.split(lines[i], ":");
|
||||
if (
|
||||
parts.length >= 2
|
||||
&& keccak256(bytes(parts[0])) == keccak256(bytes(testName))
|
||||
) {
|
||||
return vm.parseBytes(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
revert("Test calldata not found");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
267
foundry/test/TychoRouterProtocolIntegration.t.sol
Normal file
267
foundry/test/TychoRouterProtocolIntegration.t.sol
Normal file
@@ -0,0 +1,267 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "./TychoRouterTestSetup.sol";
|
||||
import "./executors/UniswapV4Utils.sol";
|
||||
|
||||
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
|
||||
function testSingleSwapUSV4CallbackPermit2() public {
|
||||
vm.startPrank(ALICE);
|
||||
uint256 amountIn = 100 ether;
|
||||
deal(USDE_ADDR, ALICE, amountIn);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(USDE_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools =
|
||||
new UniswapV4Executor.UniswapV4Pool[](1);
|
||||
pools[0] = UniswapV4Executor.UniswapV4Pool({
|
||||
intermediaryToken: USDT_ADDR,
|
||||
fee: uint24(100),
|
||||
tickSpacing: int24(1)
|
||||
});
|
||||
|
||||
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
|
||||
USDE_ADDR,
|
||||
USDT_ADDR,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL,
|
||||
ALICE,
|
||||
pools
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv4Executor), protocolData);
|
||||
|
||||
tychoRouter.singleSwapPermit2(
|
||||
amountIn,
|
||||
USDE_ADDR,
|
||||
USDT_ADDR,
|
||||
99943850,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
swap
|
||||
);
|
||||
|
||||
assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99963618);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSplitSwapMultipleUSV4Callback() public {
|
||||
// This test has two uniswap v4 hops that will be executed inside of the V4 pool manager
|
||||
// USDE -> USDT -> WBTC
|
||||
uint256 amountIn = 100 ether;
|
||||
deal(USDE_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools =
|
||||
new UniswapV4Executor.UniswapV4Pool[](2);
|
||||
pools[0] = UniswapV4Executor.UniswapV4Pool({
|
||||
intermediaryToken: USDT_ADDR,
|
||||
fee: uint24(100),
|
||||
tickSpacing: int24(1)
|
||||
});
|
||||
pools[1] = UniswapV4Executor.UniswapV4Pool({
|
||||
intermediaryToken: WBTC_ADDR,
|
||||
fee: uint24(3000),
|
||||
tickSpacing: int24(60)
|
||||
});
|
||||
|
||||
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
|
||||
USDE_ADDR,
|
||||
WBTC_ADDR,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
|
||||
ALICE,
|
||||
pools
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv4Executor), protocolData);
|
||||
|
||||
tychoRouter.singleSwap(
|
||||
amountIn, USDE_ADDR, WBTC_ADDR, 118280, false, false, ALICE, swap
|
||||
);
|
||||
|
||||
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), 118281);
|
||||
}
|
||||
|
||||
function testSequentialUSV4Integration() public {
|
||||
// Test created with calldata from our router encoder.
|
||||
|
||||
// Performs a sequential swap from USDC to PEPE though ETH using two
|
||||
// consecutive USV4 pools
|
||||
//
|
||||
// USDC ──(USV4)──> ETH ───(USV4)──> PEPE
|
||||
//
|
||||
deal(USDC_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_sequential_encoding_strategy_usv4");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 123172000092711286554274694);
|
||||
}
|
||||
|
||||
function testMultiProtocolIntegration() public {
|
||||
// Test created with calldata from our router encoder.
|
||||
//
|
||||
// DAI ─(USV2)─> WETH ─(bal)─> WBTC ─(curve)─> USDT ─(ekubo)─> ETH ─(USV4)─> USDC
|
||||
|
||||
deal(DAI_ADDR, ALICE, 1500 ether);
|
||||
uint256 balanceBefore = address(ALICE).balance;
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData = loadCallDataFromFile("test_multi_protocol");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = address(ALICE).balance;
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 732214216964381330);
|
||||
}
|
||||
|
||||
function testSingleUSV4IntegrationInputETH() public {
|
||||
// Test created with calldata from our router encoder.
|
||||
|
||||
// Performs a single swap from ETH to PEPE without wrapping or unwrapping
|
||||
//
|
||||
// ETH ───(USV4)──> PEPE
|
||||
//
|
||||
deal(ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
|
||||
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_encoding_strategy_usv4_eth_in");
|
||||
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 235610487387677804636755778);
|
||||
}
|
||||
|
||||
function testSingleUSV4IntegrationOutputETH() public {
|
||||
// Test created with calldata from our router encoder.
|
||||
|
||||
// Performs a single swap from USDC to ETH without wrapping or unwrapping
|
||||
//
|
||||
// USDC ───(USV4)──> ETH
|
||||
//
|
||||
deal(USDC_ADDR, ALICE, 3000_000000);
|
||||
uint256 balanceBefore = ALICE.balance;
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_encoding_strategy_usv4_eth_out");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = ALICE.balance;
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
console.logUint(balanceAfter - balanceBefore);
|
||||
assertEq(balanceAfter - balanceBefore, 1474406268748155809);
|
||||
}
|
||||
|
||||
function testSingleEkuboIntegration() public {
|
||||
vm.stopPrank();
|
||||
|
||||
deal(ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_encoding_strategy_ekubo");
|
||||
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertGe(balanceAfter - balanceBefore, 26173932);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSingleCurveIntegration() public {
|
||||
deal(UWU_ADDR, ALICE, 1 ether);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_encoding_strategy_curve");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 2877855391767);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSingleSwapUSV3Permit2() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2
|
||||
// Tests entire USV3 flow including callback
|
||||
// 1 WETH -> DAI
|
||||
// (USV3)
|
||||
vm.startPrank(ALICE);
|
||||
uint256 amountIn = 10 ** 18;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
||||
bool zeroForOne = false;
|
||||
bytes memory protocolData = encodeUniswapV3Swap(
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
ALICE,
|
||||
DAI_WETH_USV3,
|
||||
zeroForOne,
|
||||
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
);
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv3Executor), protocolData);
|
||||
|
||||
tychoRouter.singleSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
expAmountOut - 1,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
swap
|
||||
);
|
||||
|
||||
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertGe(finalBalance, expAmountOut);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
}
|
||||
498
foundry/test/TychoRouterSequentialSwap.t.sol
Normal file
498
foundry/test/TychoRouterSequentialSwap.t.sol
Normal file
@@ -0,0 +1,498 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@src/executors/UniswapV4Executor.sol";
|
||||
import {TychoRouter} from "@src/TychoRouter.sol";
|
||||
import "./TychoRouterTestSetup.sol";
|
||||
import "./executors/UniswapV4Utils.sol";
|
||||
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
|
||||
|
||||
contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
|
||||
function _getSequentialSwaps(bool permit2)
|
||||
internal
|
||||
view
|
||||
returns (bytes[] memory)
|
||||
{
|
||||
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
|
||||
// 1 WETH -> DAI -> USDC
|
||||
// (univ2) (univ2)
|
||||
|
||||
TokenTransfer.TransferType transferType = permit2
|
||||
? TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
: TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL;
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
// WETH -> DAI
|
||||
swaps[0] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false, transferType
|
||||
)
|
||||
);
|
||||
|
||||
// DAI -> USDC
|
||||
swaps[1] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
DAI_ADDR,
|
||||
DAI_USDC_POOL,
|
||||
ALICE,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
return swaps;
|
||||
}
|
||||
|
||||
function testSequentialSwapPermit2() public {
|
||||
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSequentialSwaps(true);
|
||||
tychoRouter.sequentialSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
1000_000000, // min amount
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 2005810530);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSequentialSwapNoPermit2() public {
|
||||
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSequentialSwaps(false);
|
||||
tychoRouter.sequentialSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
1000_000000, // min amount
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 2005810530);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSequentialSwapUndefinedMinAmount() public {
|
||||
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSequentialSwaps(false);
|
||||
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
|
||||
tychoRouter.sequentialSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
0, // min amount
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
}
|
||||
|
||||
function testSequentialSwapInsufficientApproval() public {
|
||||
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn - 1);
|
||||
|
||||
bytes[] memory swaps = _getSequentialSwaps(false);
|
||||
vm.expectRevert();
|
||||
tychoRouter.sequentialSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
0, // min amount
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
}
|
||||
|
||||
function testSequentialSwapNegativeSlippageFailure() public {
|
||||
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSequentialSwaps(true);
|
||||
|
||||
uint256 minAmountOut = 3000 * 1e18;
|
||||
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(
|
||||
TychoRouter__NegativeSlippage.selector,
|
||||
2005810530, // actual amountOut
|
||||
minAmountOut
|
||||
)
|
||||
);
|
||||
tychoRouter.sequentialSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSequentialSwapWrapETH() public {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
IAllowanceTransfer.PermitSingle memory emptyPermitSingle =
|
||||
IAllowanceTransfer.PermitSingle({
|
||||
details: IAllowanceTransfer.PermitDetails({
|
||||
token: address(0),
|
||||
amount: 0,
|
||||
expiration: 0,
|
||||
nonce: 0
|
||||
}),
|
||||
spender: address(0),
|
||||
sigDeadline: 0
|
||||
});
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
// WETH -> DAI
|
||||
swaps[0] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
tychoRouterAddr,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
|
||||
// DAI -> USDC
|
||||
swaps[1] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
DAI_ADDR,
|
||||
DAI_USDC_POOL,
|
||||
ALICE,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
|
||||
uint256 amountOut = tychoRouter.sequentialSwapPermit2{value: amountIn}(
|
||||
amountIn,
|
||||
address(0),
|
||||
USDC_ADDR,
|
||||
1000_000000,
|
||||
true,
|
||||
false,
|
||||
ALICE,
|
||||
emptyPermitSingle,
|
||||
"",
|
||||
pleEncode(swaps)
|
||||
);
|
||||
uint256 expectedAmount = 2005810530;
|
||||
assertEq(amountOut, expectedAmount);
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, expectedAmount);
|
||||
assertEq(ALICE.balance, 0);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSequentialSwapUnwrapETH() public {
|
||||
// Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end
|
||||
|
||||
uint256 amountIn = 3_000 * 10 ** 6;
|
||||
deal(USDC_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(USDC_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
|
||||
// USDC -> DAI
|
||||
swaps[0] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
USDC_ADDR,
|
||||
DAI_USDC_POOL,
|
||||
tychoRouterAddr,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
|
||||
// DAI -> WETH
|
||||
swaps[1] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
DAI_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
tychoRouterAddr,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
|
||||
uint256 amountOut = tychoRouter.sequentialSwapPermit2(
|
||||
amountIn,
|
||||
USDC_ADDR,
|
||||
address(0),
|
||||
1 * 10 ** 18, // min amount
|
||||
false,
|
||||
true,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 expectedAmount = 1466332452295613768; // 1.11 ETH
|
||||
assertEq(amountOut, expectedAmount);
|
||||
assertEq(ALICE.balance, expectedAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testCyclicSequentialSwap() public {
|
||||
// This test has start and end tokens that are the same
|
||||
// The flow is:
|
||||
// USDC --(USV3)--> WETH --(USV3)--> USDC
|
||||
uint256 amountIn = 100 * 10 ** 6;
|
||||
deal(USDC_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
|
||||
USDC_ADDR,
|
||||
WETH_ADDR,
|
||||
tychoRouterAddr,
|
||||
USDC_WETH_USV3,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
tychoRouterAddr,
|
||||
USDC_WETH_USV3_2,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
// USDC -> WETH
|
||||
swaps[0] = encodeSequentialSwap(
|
||||
address(usv3Executor), usdcWethV3Pool1ZeroOneData
|
||||
);
|
||||
// WETH -> USDC
|
||||
swaps[1] = encodeSequentialSwap(
|
||||
address(usv3Executor), usdcWethV3Pool2OneZeroData
|
||||
);
|
||||
|
||||
tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps));
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99792554);
|
||||
}
|
||||
|
||||
function testSequentialSwapIntegrationPermit2() public {
|
||||
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
|
||||
//
|
||||
// WETH ──(USV2)──> WBTC ───(USV2)──> USDC
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_sequential_swap_strategy_encoder");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 1951856272);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSequentialSwapIntegration() public {
|
||||
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
|
||||
//
|
||||
// WETH ──(USV2)──> WBTC ───(USV2)──> USDC
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData = loadCallDataFromFile(
|
||||
"test_sequential_swap_strategy_encoder_no_permit2"
|
||||
);
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 1951856272);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSequentialCyclicSwapIntegration() public {
|
||||
// USDC -> WETH -> USDC using two pools
|
||||
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_sequential_strategy_cyclic_swap");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99792554);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testUSV3USV2Integration() public {
|
||||
// Performs a sequential swap from WETH to USDC though WBTC and DAI using USV3 and USV2 pools
|
||||
//
|
||||
// WETH ──(USV3)──> WBTC ───(USV2)──> USDC
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_uniswap_v3_uniswap_v2");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 1952973189);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testUSV3USV3Integration() public {
|
||||
// Performs a sequential swap from WETH to USDC though WBTC using USV3 pools
|
||||
//
|
||||
// WETH ──(USV3)──> WBTC ───(USV3)──> USDC
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_uniswap_v3_uniswap_v3");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 2015740345);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testUSV3CurveIntegration() public {
|
||||
// Performs a sequential swap from WETH to USDT though WBTC using USV3 and Curve pools
|
||||
//
|
||||
// WETH ──(USV3)──> WBTC ───(USV3)──> USDT
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDT_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_curve");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDT_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 2018869128);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testBalancerV2USV2Integration() public {
|
||||
// Performs a sequential swap from WETH to USDC though WBTC using Balancer v2 and USV2 pools
|
||||
//
|
||||
// WETH ──(balancer)──> WBTC ───(USV2)──> USDC
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDT_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_balancer_v2_uniswap_v2");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 1949668893);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
}
|
||||
384
foundry/test/TychoRouterSingleSwap.t.sol
Normal file
384
foundry/test/TychoRouterSingleSwap.t.sol
Normal file
@@ -0,0 +1,384 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@src/executors/UniswapV4Executor.sol";
|
||||
import {TychoRouter} from "@src/TychoRouter.sol";
|
||||
import "./TychoRouterTestSetup.sol";
|
||||
import "./executors/UniswapV4Utils.sol";
|
||||
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
|
||||
|
||||
contract TychoRouterSingleSwapTest is TychoRouterTestSetup {
|
||||
function testSingleSwapPermit2() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2
|
||||
// 1 WETH -> DAI
|
||||
// (USV2)
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
tychoRouter.singleSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
2008817438608734439722,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
swap
|
||||
);
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, 2018817438608734439722);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSingleSwapNoPermit2() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||
// Checks amount out at the end
|
||||
uint256 amountIn = 1 ether;
|
||||
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
// Approve the tokenIn to be transferred to the router
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 minAmountOut = 2000 * 1e18;
|
||||
uint256 amountOut = tychoRouter.singleSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
swap
|
||||
);
|
||||
|
||||
uint256 expectedAmount = 2018817438608734439722;
|
||||
assertEq(amountOut, expectedAmount);
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, expectedAmount);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSingleSwapUndefinedMinAmount() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||
// Checks amount out at the end
|
||||
uint256 amountIn = 1 ether;
|
||||
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
|
||||
tychoRouter.singleSwap(
|
||||
amountIn, WETH_ADDR, DAI_ADDR, 0, false, false, ALICE, swap
|
||||
);
|
||||
}
|
||||
|
||||
function testSingleSwapInsufficientApproval() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||
// Checks amount out at the end
|
||||
uint256 amountIn = 1 ether;
|
||||
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 minAmountOut = 2600 * 1e18;
|
||||
vm.expectRevert();
|
||||
tychoRouter.singleSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
swap
|
||||
);
|
||||
}
|
||||
|
||||
function testSingleSwapNegativeSlippageFailure() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||
// Checks amount out at the end
|
||||
uint256 amountIn = 1 ether;
|
||||
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
// Approve the tokenIn to be transferred to the router
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 minAmountOut = 5600 * 1e18;
|
||||
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(
|
||||
TychoRouter__NegativeSlippage.selector,
|
||||
2018817438608734439722, // actual amountOut
|
||||
minAmountOut
|
||||
)
|
||||
);
|
||||
tychoRouter.singleSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
ALICE,
|
||||
swap
|
||||
);
|
||||
}
|
||||
|
||||
function testSingleSwapWrapETH() public {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
IAllowanceTransfer.PermitSingle memory emptyPermitSingle =
|
||||
IAllowanceTransfer.PermitSingle({
|
||||
details: IAllowanceTransfer.PermitDetails({
|
||||
token: address(0),
|
||||
amount: 0,
|
||||
expiration: 0,
|
||||
nonce: 0
|
||||
}),
|
||||
spender: address(0),
|
||||
sigDeadline: 0
|
||||
});
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 amountOut = tychoRouter.singleSwapPermit2{value: amountIn}(
|
||||
amountIn,
|
||||
address(0),
|
||||
DAI_ADDR,
|
||||
1000_000000,
|
||||
true,
|
||||
false,
|
||||
ALICE,
|
||||
emptyPermitSingle,
|
||||
"",
|
||||
swap
|
||||
);
|
||||
uint256 expectedAmount = 2018817438608734439722;
|
||||
assertEq(amountOut, expectedAmount);
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, expectedAmount);
|
||||
assertEq(ALICE.balance, 0);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSingleSwapUnwrapETH() public {
|
||||
// DAI -> WETH with unwrapping to ETH
|
||||
uint256 amountIn = 3000 ether;
|
||||
deal(DAI_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(DAI_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
DAI_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
tychoRouterAddr,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 amountOut = tychoRouter.singleSwapPermit2(
|
||||
amountIn,
|
||||
DAI_ADDR,
|
||||
address(0),
|
||||
1000_000000,
|
||||
false,
|
||||
true,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
swap
|
||||
);
|
||||
|
||||
uint256 expectedAmount = 1475644707225677606;
|
||||
assertEq(amountOut, expectedAmount);
|
||||
assertEq(ALICE.balance, expectedAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSingleSwapIntegration() public {
|
||||
// Tests swapping WETH -> DAI on a USV2 pool with regular approvals
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_swap_strategy_encoder_no_permit2");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
|
||||
}
|
||||
|
||||
function testSingleSwapIntegrationPermit2() public {
|
||||
// Tests swapping WETH -> DAI on a USV2 pool with permit2
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_swap_strategy_encoder");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
|
||||
}
|
||||
|
||||
function testSingleSwapWithWrapIntegration() public {
|
||||
// Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user
|
||||
// and wrapped before the swap
|
||||
deal(ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_swap_strategy_encoder_wrap");
|
||||
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
|
||||
}
|
||||
|
||||
function testSingleSwapWithUnwrapIntegration() public {
|
||||
// Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH
|
||||
// before sending back to the user
|
||||
deal(DAI_ADDR, ALICE, 3000 ether);
|
||||
uint256 balanceBefore = ALICE.balance;
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_swap_strategy_encoder_unwrap");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = ALICE.balance;
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 1475644707225677606);
|
||||
}
|
||||
|
||||
function testSingleSwapIntegrationNoTransferIn() public {
|
||||
// Tests swapping WETH -> DAI on a USV2 pool assuming that the tokens are already inside the router
|
||||
deal(WETH_ADDR, tychoRouterAddr, 1 ether);
|
||||
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
bytes memory callData = loadCallDataFromFile(
|
||||
"test_single_swap_strategy_encoder_no_transfer_in"
|
||||
);
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
|
||||
}
|
||||
}
|
||||
578
foundry/test/TychoRouterSplitSwap.t.sol
Normal file
578
foundry/test/TychoRouterSplitSwap.t.sol
Normal file
@@ -0,0 +1,578 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@src/executors/UniswapV4Executor.sol";
|
||||
import {TychoRouter} from "@src/TychoRouter.sol";
|
||||
import "./TychoRouterTestSetup.sol";
|
||||
import "./executors/UniswapV4Utils.sol";
|
||||
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
|
||||
|
||||
contract TychoRouterSplitSwapTest is TychoRouterTestSetup {
|
||||
function _getSplitSwaps(bool permit2)
|
||||
private
|
||||
view
|
||||
returns (bytes[] memory)
|
||||
{
|
||||
// Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2
|
||||
// -> DAI ->
|
||||
// 1 WETH USDC
|
||||
// -> WBTC ->
|
||||
// (univ2) (univ2)
|
||||
bytes[] memory swaps = new bytes[](4);
|
||||
|
||||
TokenTransfer.TransferType inTransferType = permit2
|
||||
? TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
: TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL;
|
||||
|
||||
// WETH -> WBTC (60%)
|
||||
swaps[0] = encodeSplitSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
(0xffffff * 60) / 100, // 60%
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_WBTC_POOL,
|
||||
tychoRouterAddr,
|
||||
false,
|
||||
inTransferType
|
||||
)
|
||||
);
|
||||
// WBTC -> USDC
|
||||
swaps[1] = encodeSplitSwap(
|
||||
uint8(1),
|
||||
uint8(2),
|
||||
uint24(0),
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
WBTC_ADDR,
|
||||
USDC_WBTC_POOL,
|
||||
ALICE,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
// WETH -> DAI
|
||||
swaps[2] = encodeSplitSwap(
|
||||
uint8(0),
|
||||
uint8(3),
|
||||
uint24(0),
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false, inTransferType
|
||||
)
|
||||
);
|
||||
|
||||
// DAI -> USDC
|
||||
swaps[3] = encodeSplitSwap(
|
||||
uint8(3),
|
||||
uint8(2),
|
||||
uint24(0),
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
DAI_ADDR,
|
||||
DAI_USDC_POOL,
|
||||
ALICE,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
)
|
||||
);
|
||||
|
||||
return swaps;
|
||||
}
|
||||
|
||||
function testSplitSwapInternalMethod() public {
|
||||
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
|
||||
bytes[] memory swaps = _getSplitSwaps(false);
|
||||
tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps));
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 1989737355);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSplitSwapPermit2() public {
|
||||
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSplitSwaps(true);
|
||||
|
||||
tychoRouter.splitSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
1, // min amount
|
||||
false,
|
||||
false,
|
||||
4,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 1989737355);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSplitSwapNoPermit2() public {
|
||||
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSplitSwaps(false);
|
||||
|
||||
tychoRouter.splitSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
1000_000000, // min amount
|
||||
false,
|
||||
false,
|
||||
4,
|
||||
ALICE,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 1989737355);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
||||
}
|
||||
|
||||
function testSplitSwapUndefinedMinAmount() public {
|
||||
// Min amount should always be non-zero. If zero, swap attempt should revert.
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSplitSwaps(false);
|
||||
|
||||
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
|
||||
tychoRouter.splitSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
0, // min amount
|
||||
false,
|
||||
false,
|
||||
4,
|
||||
ALICE,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSplitSwapInsufficientApproval() public {
|
||||
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
|
||||
uint256 amountIn = 1 ether;
|
||||
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
// Approve less than the amountIn
|
||||
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1);
|
||||
bytes[] memory swaps = _getSplitSwaps(false);
|
||||
|
||||
vm.expectRevert();
|
||||
tychoRouter.splitSwap(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
1000_000000, // min amount
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSplitSwapNegativeSlippageFailure() public {
|
||||
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes[] memory swaps = _getSplitSwaps(true);
|
||||
|
||||
uint256 minAmountOut = 3000 * 1e18;
|
||||
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(
|
||||
TychoRouter__NegativeSlippage.selector,
|
||||
1989737355, // actual amountOut
|
||||
minAmountOut
|
||||
)
|
||||
);
|
||||
tychoRouter.splitSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
4,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSplitSwapWrapETH() public {
|
||||
// Trade 1 ETH (and wrap it) for DAI with 1 swap on Uniswap V2
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
IAllowanceTransfer.PermitSingle memory emptyPermitSingle =
|
||||
IAllowanceTransfer.PermitSingle({
|
||||
details: IAllowanceTransfer.PermitDetails({
|
||||
token: address(0),
|
||||
amount: 0,
|
||||
expiration: 0,
|
||||
nonce: 0
|
||||
}),
|
||||
spender: address(0),
|
||||
sigDeadline: 0
|
||||
});
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap = encodeSplitSwap(
|
||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
||||
);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 amountOut = tychoRouter.splitSwapPermit2{value: amountIn}(
|
||||
amountIn,
|
||||
address(0),
|
||||
DAI_ADDR,
|
||||
2008817438608734439722,
|
||||
true,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
emptyPermitSingle,
|
||||
"",
|
||||
pleEncode(swaps)
|
||||
);
|
||||
uint256 expectedAmount = 2018817438608734439722;
|
||||
assertEq(amountOut, expectedAmount);
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, expectedAmount);
|
||||
assertEq(ALICE.balance, 0);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSplitSwapUnwrapETH() public {
|
||||
// Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end
|
||||
|
||||
uint256 amountIn = 3_000 * 10 ** 18;
|
||||
deal(DAI_ADDR, ALICE, amountIn);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(DAI_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
DAI_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
tychoRouterAddr,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap = encodeSplitSwap(
|
||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
||||
);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 amountOut = tychoRouter.splitSwapPermit2(
|
||||
amountIn,
|
||||
DAI_ADDR,
|
||||
address(0),
|
||||
1465644707225677606,
|
||||
false,
|
||||
true,
|
||||
2,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 expectedAmount = 1475644707225677606; // 1.12 ETH
|
||||
assertEq(amountOut, expectedAmount);
|
||||
assertEq(ALICE.balance, expectedAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testEmptySwapsRevert() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
bytes memory swaps = "";
|
||||
vm.expectRevert(TychoRouter__EmptySwaps.selector);
|
||||
tychoRouter.exposedSplitSwap(amountIn, 2, swaps);
|
||||
}
|
||||
|
||||
function testSplitInputCyclicSwapInternalMethod() public {
|
||||
// This test has start and end tokens that are the same
|
||||
// The flow is:
|
||||
// ┌─ (USV3, 60% split) ──> WETH ─┐
|
||||
// │ │
|
||||
// USDC ──────┤ ├──(USV2)──> USDC
|
||||
// │ │
|
||||
// └─ (USV3, 40% split) ──> WETH ─┘
|
||||
uint256 amountIn = 100 * 10 ** 6;
|
||||
deal(USDC_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
// Approve the TychoRouter to spend USDC
|
||||
IERC20(USDC_ADDR).approve(tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
|
||||
USDC_ADDR,
|
||||
WETH_ADDR,
|
||||
tychoRouterAddr,
|
||||
USDC_WETH_USV3,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap(
|
||||
USDC_ADDR,
|
||||
WETH_ADDR,
|
||||
tychoRouterAddr,
|
||||
USDC_WETH_USV3_2,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap(
|
||||
WETH_ADDR,
|
||||
USDC_WETH_USV2,
|
||||
tychoRouterAddr,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes[] memory swaps = new bytes[](3);
|
||||
// USDC -> WETH (60% split)
|
||||
swaps[0] = encodeSplitSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
(0xffffff * 60) / 100, // 60%
|
||||
address(usv3Executor),
|
||||
usdcWethV3Pool1ZeroOneData
|
||||
);
|
||||
// USDC -> WETH (40% remainder)
|
||||
swaps[1] = encodeSplitSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
uint24(0),
|
||||
address(usv3Executor),
|
||||
usdcWethV3Pool2ZeroOneData
|
||||
);
|
||||
// WETH -> USDC
|
||||
swaps[2] = encodeSplitSwap(
|
||||
uint8(1),
|
||||
uint8(0),
|
||||
uint24(0),
|
||||
address(usv2Executor),
|
||||
wethUsdcV2OneZeroData
|
||||
);
|
||||
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
|
||||
vm.stopPrank();
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99654537);
|
||||
}
|
||||
|
||||
function testSplitOutputCyclicSwapInternalMethod() public {
|
||||
// This test has start and end tokens that are the same
|
||||
// The flow is:
|
||||
// ┌─── (USV3, 60% split) ───┐
|
||||
// │ │
|
||||
// USDC ──(USV2) ── WETH──| ├─> USDC
|
||||
// │ │
|
||||
// └─── (USV3, 40% split) ───┘
|
||||
|
||||
uint256 amountIn = 100 * 10 ** 6;
|
||||
deal(USDC_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory usdcWethV2Data = encodeUniswapV2Swap(
|
||||
USDC_ADDR,
|
||||
USDC_WETH_USV2,
|
||||
tychoRouterAddr,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap(
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
tychoRouterAddr,
|
||||
USDC_WETH_USV3,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
tychoRouterAddr,
|
||||
USDC_WETH_USV3_2,
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes[] memory swaps = new bytes[](3);
|
||||
// USDC -> WETH
|
||||
swaps[0] = encodeSplitSwap(
|
||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data
|
||||
);
|
||||
// WETH -> USDC
|
||||
swaps[1] = encodeSplitSwap(
|
||||
uint8(1),
|
||||
uint8(0),
|
||||
(0xffffff * 60) / 100,
|
||||
address(usv3Executor),
|
||||
usdcWethV3Pool1OneZeroData
|
||||
);
|
||||
|
||||
// WETH -> USDC
|
||||
swaps[2] = encodeSplitSwap(
|
||||
uint8(1),
|
||||
uint8(0),
|
||||
uint24(0),
|
||||
address(usv3Executor),
|
||||
usdcWethV3Pool2OneZeroData
|
||||
);
|
||||
|
||||
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99444510);
|
||||
}
|
||||
|
||||
// Base Network Tests
|
||||
// Make sure to set the RPC_URL to base network
|
||||
function testSplitSwapInternalMethodBase() public {
|
||||
vm.skip(true);
|
||||
vm.rollFork(26857267);
|
||||
uint256 amountIn = 10 * 10 ** 6;
|
||||
deal(BASE_USDC, tychoRouterAddr, amountIn);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
BASE_USDC,
|
||||
USDC_MAG7_POOL,
|
||||
tychoRouterAddr,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
|
||||
);
|
||||
|
||||
bytes memory swap = encodeSplitSwap(
|
||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
||||
);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
|
||||
assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606);
|
||||
}
|
||||
|
||||
function testSplitSwapIntegration() public {
|
||||
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
|
||||
//
|
||||
// ┌──(USV2)──> WBTC ───(USV2)──> USDC
|
||||
// WETH ─┤
|
||||
// └──(USV2)──> DAI ───(USV2)──> USDC
|
||||
deal(WETH_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_split_swap_strategy_encoder");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertGe(balanceAfter - balanceBefore, 26173932);
|
||||
|
||||
// All input tokens are transferred to the router at first. Make sure we used
|
||||
// all of it (and thus our splits are correct).
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSplitInputCyclicSwapIntegration() public {
|
||||
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_split_input_cyclic_swap");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99654537);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSplitOutputCyclicSwapIntegration() public {
|
||||
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_split_output_cyclic_swap");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99444510);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import "@src/TychoRouter.sol";
|
||||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
||||
import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol";
|
||||
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
|
||||
import {Permit2TestHelper} from "./Permit2TestHelper.sol";
|
||||
import "./TestUtils.sol";
|
||||
|
||||
contract TychoRouterExposed is TychoRouter {
|
||||
constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {}
|
||||
@@ -25,16 +27,23 @@ contract TychoRouterExposed is TychoRouter {
|
||||
return _unwrapETH(amount);
|
||||
}
|
||||
|
||||
function exposedSwap(
|
||||
function exposedSplitSwap(
|
||||
uint256 amountIn,
|
||||
uint256 nTokens,
|
||||
bytes calldata swaps
|
||||
) external returns (uint256) {
|
||||
return _swap(amountIn, nTokens, swaps);
|
||||
return _splitSwap(amountIn, nTokens, swaps);
|
||||
}
|
||||
|
||||
function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps)
|
||||
external
|
||||
returns (uint256)
|
||||
{
|
||||
return _sequentialSwap(amountIn, swaps);
|
||||
}
|
||||
}
|
||||
|
||||
contract TychoRouterTestSetup is Test, Constants {
|
||||
contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
TychoRouterExposed tychoRouter;
|
||||
address tychoRouterAddr;
|
||||
UniswapV2Executor public usv2Executor;
|
||||
@@ -47,7 +56,7 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
MockERC20[] tokens;
|
||||
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 21817316;
|
||||
uint256 forkBlock = 22082754;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
|
||||
vm.startPrank(ADMIN);
|
||||
@@ -71,7 +80,6 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
tychoRouter = new TychoRouterExposed(PERMIT2_ADDRESS, WETH_ADDR);
|
||||
tychoRouterAddr = address(tychoRouter);
|
||||
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
|
||||
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
||||
tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER);
|
||||
tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER);
|
||||
tychoRouter.grantRole(
|
||||
@@ -91,14 +99,17 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
|
||||
|
||||
IPoolManager poolManager = IPoolManager(poolManagerAddress);
|
||||
usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2);
|
||||
usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3);
|
||||
usv4Executor = new UniswapV4Executor(poolManager);
|
||||
pancakev3Executor =
|
||||
new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3);
|
||||
balancerv2Executor = new BalancerV2Executor();
|
||||
ekuboExecutor = new EkuboExecutor(ekuboCore);
|
||||
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE);
|
||||
usv2Executor =
|
||||
new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS, 30);
|
||||
usv3Executor =
|
||||
new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS);
|
||||
usv4Executor = new UniswapV4Executor(poolManager, PERMIT2_ADDRESS);
|
||||
pancakev3Executor = new UniswapV3Executor(
|
||||
factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS
|
||||
);
|
||||
balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS);
|
||||
ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS);
|
||||
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
|
||||
|
||||
address[] memory executors = new address[](7);
|
||||
executors[0] = address(usv2Executor);
|
||||
@@ -123,84 +134,6 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Handles the Permit2 approval process for Alice, allowing the TychoRouter contract
|
||||
* to spend `amount_in` of `tokenIn` on her behalf.
|
||||
*
|
||||
* This function approves the Permit2 contract to transfer the specified token amount
|
||||
* and constructs a `PermitSingle` struct for the approval. It also generates a valid
|
||||
* EIP-712 signature for the approval using Alice's private key.
|
||||
*
|
||||
* @param tokenIn The address of the token being approved.
|
||||
* @param amount_in The amount of tokens to approve for transfer.
|
||||
* @return permitSingle The `PermitSingle` struct containing the approval details.
|
||||
* @return signature The EIP-712 signature for the approval.
|
||||
*/
|
||||
function handlePermit2Approval(address tokenIn, uint256 amount_in)
|
||||
internal
|
||||
returns (IAllowanceTransfer.PermitSingle memory, bytes memory)
|
||||
{
|
||||
IERC20(tokenIn).approve(PERMIT2_ADDRESS, amount_in);
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer
|
||||
.PermitSingle({
|
||||
details: IAllowanceTransfer.PermitDetails({
|
||||
token: tokenIn,
|
||||
amount: uint160(amount_in),
|
||||
expiration: uint48(block.timestamp + 1 days),
|
||||
nonce: 0
|
||||
}),
|
||||
spender: tychoRouterAddr,
|
||||
sigDeadline: block.timestamp + 1 days
|
||||
});
|
||||
|
||||
bytes memory signature = signPermit2(permitSingle, ALICE_PK);
|
||||
return (permitSingle, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Signs a Permit2 `PermitSingle` struct with the given private key.
|
||||
* @param permit The `PermitSingle` struct to sign.
|
||||
* @param privateKey The private key of the signer.
|
||||
* @return The signature as a `bytes` array.
|
||||
*/
|
||||
function signPermit2(
|
||||
IAllowanceTransfer.PermitSingle memory permit,
|
||||
uint256 privateKey
|
||||
) internal view returns (bytes memory) {
|
||||
bytes32 _PERMIT_DETAILS_TYPEHASH = keccak256(
|
||||
"PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||
);
|
||||
bytes32 _PERMIT_SINGLE_TYPEHASH = keccak256(
|
||||
"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||
);
|
||||
bytes32 domainSeparator = keccak256(
|
||||
abi.encode(
|
||||
keccak256(
|
||||
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
|
||||
),
|
||||
keccak256("Permit2"),
|
||||
block.chainid,
|
||||
PERMIT2_ADDRESS
|
||||
)
|
||||
);
|
||||
bytes32 detailsHash =
|
||||
keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, permit.details));
|
||||
bytes32 permitHash = keccak256(
|
||||
abi.encode(
|
||||
_PERMIT_SINGLE_TYPEHASH,
|
||||
detailsHash,
|
||||
permit.spender,
|
||||
permit.sigDeadline
|
||||
)
|
||||
);
|
||||
|
||||
bytes32 digest =
|
||||
keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitHash));
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
|
||||
|
||||
return abi.encodePacked(r, s, v);
|
||||
}
|
||||
|
||||
function pleEncode(bytes[] memory data)
|
||||
public
|
||||
pure
|
||||
@@ -214,7 +147,23 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
}
|
||||
}
|
||||
|
||||
function encodeSwap(
|
||||
function encodeSingleSwap(address executor, bytes memory protocolData)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodePacked(executor, protocolData);
|
||||
}
|
||||
|
||||
function encodeSequentialSwap(address executor, bytes memory protocolData)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodePacked(executor, protocolData);
|
||||
}
|
||||
|
||||
function encodeSplitSwap(
|
||||
uint8 tokenInIndex,
|
||||
uint8 tokenOutIndex,
|
||||
uint24 split,
|
||||
@@ -230,9 +179,11 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
address tokenIn,
|
||||
address target,
|
||||
address receiver,
|
||||
bool zero2one
|
||||
bool zero2one,
|
||||
TokenTransfer.TransferType transferType
|
||||
) internal pure returns (bytes memory) {
|
||||
return abi.encodePacked(tokenIn, target, receiver, zero2one);
|
||||
return
|
||||
abi.encodePacked(tokenIn, target, receiver, zero2one, transferType);
|
||||
}
|
||||
|
||||
function encodeUniswapV3Swap(
|
||||
@@ -240,11 +191,18 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
address tokenOut,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zero2one
|
||||
bool zero2one,
|
||||
TokenTransfer.TransferType transferType
|
||||
) internal view returns (bytes memory) {
|
||||
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||
return abi.encodePacked(
|
||||
tokenIn, tokenOut, pool.fee(), receiver, target, zero2one
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
pool.fee(),
|
||||
receiver,
|
||||
target,
|
||||
zero2one,
|
||||
transferType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
26
foundry/test/assets/calldata.txt
Normal file
26
foundry/test/assets/calldata.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
test_uniswap_v3_uniswap_v2:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000bf00692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb8004375dff511095cc5a197a54140a24efef3a416cbcdf9626bc03e24f779434178a73a0b4bad62ed000100525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010500
|
||||
test_single_encoding_strategy_ekubo:20144a070000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000071a0cb889707d426a7a386870a03bc70d1b069759805cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000000000000000
|
||||
test_uniswap_v3_uniswap_v3:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000100692e234dae75c793f67a35089c9d99245e1c58470b2260fac5e5542a773aa44fbcfedf7c193bc2c599a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc299ac8ca7087fa4a2a1fb6357269965a2014abc35010000000000000000000000
|
||||
test_balancer_v2_uniswap_v2:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c80072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e004375dff511095cc5a197a54140a24efef3a416010300525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000
|
||||
test_sequential_swap_strategy_encoder_no_permit2:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000100525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000
|
||||
test_sequential_encoding_strategy_usv4:51bcc7b6000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004182b5da1843c415fd1304215c3049ced9c5e9b1f66463d5b035d67eb6fe8903de2564f5cb6740cdd838d7e497c61a75ccc572273a67e8c4b9e63f5f1ffc31e3bb1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000880086f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f4000000000000000000000000000000000000000000000000
|
||||
test_single_encoding_strategy_usv4_eth_out:7c55384600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a0900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000418b43338a724ecb56c3c2ba7b10f26445ef901fa89fb585ad97771b24b87b2d71627d7dcd178bce3594dff4e4c024bd34c64ca33eda7ed6419c627e9a9b089e591c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007300710001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c00000000000000000000000000
|
||||
test_sequential_swap_strategy_encoder:51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000411ddba9e27afa444896a84afcbd773cd492da98731f40e28243bf0aae4f345b7d44b7a6d220f7e5345055b7fe382114f0bf9f1ddb9e97987515880311f25f001b1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000200525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000
|
||||
test_single_swap_strategy_encoder_no_permit2:20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000
|
||||
test_single_swap_strategy_encoder_no_transfer_in:20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000
|
||||
test_single_encoding_strategy_usv4_eth_in:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041657dbf542ed8b39f717c204413c77002d0d75fb3f81300bfef8eedf5889b55523868c42c0e2ef53fe7b82229ceceabe1b662201898043519d042e333c7492dad1c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330105cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc26982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000
|
||||
test_sequential_strategy_cyclic_swap:51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041de5b09c674e85fef9de204fd3a03e190b1d06ebb6847543298183db02b46c0123c1433aec9951dfa391bc489c756816c4e87851882c8b4adbd21931490a820781b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010200692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000000000000000000000000
|
||||
test_single_encoding_strategy_curve_st_eth:20144a070000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe84000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f670220100010005cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000
|
||||
test_single_swap_strategy_encoder:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006b56051582a970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041df0cad7d6d1228221ff55af2259877bbc38b6ef4af79f90757eab0576601a2f522e27881b929a5053eccb0d2fddc18db0bf3e905e407b5f17461e1bf64cbe70e1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200020000000000000000000000000000
|
||||
test_single_encoding_strategy_curve:20144a070000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e710201000103cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000
|
||||
test_single_swap_strategy_encoder_unwrap:30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be00000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004148c4a9386769cda15972ca351ee585b996d184bcf3539441f103ca8477ddc35b4f34e91f490a181166440ceabe512ecc71bd00615ef5ba0ac5a7c58f2e0653511c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501020000000000000000000000000000
|
||||
test_single_swap_strategy_encoder_wrap:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041ca7017e254f4fc333dbe49435283ee7d7bbf0ab261f79a3606848abbc7387dc2563fcdb32e670f1f80e729326d8d9973aef060a7f7e28d9c8ec2420cad05aa6f1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000
|
||||
test_split_output_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004177fcc3f010414cde9910d59346b7cac5341063addb7f041740fd0851cff3cd914b8b63c61edabe968f912b2ed17f273221a995d049e5d198384b5dbd00edb2011c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950102006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400000006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000000000000000000
|
||||
test_split_input_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004177fcc3f010414cde9910d59346b7cac5341063addb7f041740fd0851cff3cd914b8b63c61edabe968f912b2ed17f273221a995d049e5d198384b5dbd00edb2011c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400102006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80102005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000
|
||||
test_split_swap_strategy_encoder:7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a091000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041df0cad7d6d1228221ff55af2259877bbc38b6ef4af79f90757eab0576601a2f522e27881b929a5053eccb0d2fddc18db0bf3e905e407b5f17461e1bf64cbe70e1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950002005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950002005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20100005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010000000000000000000000000000000000000000000000000000000000
|
||||
test_uniswap_v3_curve:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000100691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae460301000105cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000
|
||||
test_multi_protocol:51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf00000000000000000000000000000000000000000000000000000000000006838268a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09200000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041cf9d1ecdf2b9df43bdac811677440db893f99f04f5addb24c82b15d766c87e5c0091919a2c43ed9dc6fbd1237ce2bed73a3ea27cd638af11c1bb5673468e1d051b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021400525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501020072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010500691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001053ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598003ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c43200000032006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c000000000000000000000000
|
||||
test_encode_balancer_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105
|
||||
test_ekubo_encode_swap_multi:00ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032
|
||||
test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c
|
||||
test_encode_uniswap_v4_simple_swap:4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7000064000001
|
||||
@@ -1,11 +1,13 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "../TestUtils.sol";
|
||||
import "@src/executors/BalancerV2Executor.sol";
|
||||
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||
import {Constants} from "../Constants.sol";
|
||||
|
||||
contract BalancerV2ExecutorExposed is BalancerV2Executor {
|
||||
constructor(address _permit2) BalancerV2Executor(_permit2) {}
|
||||
|
||||
function decodeParams(bytes calldata data)
|
||||
external
|
||||
pure
|
||||
@@ -14,18 +16,15 @@ contract BalancerV2ExecutorExposed is BalancerV2Executor {
|
||||
IERC20 tokenOut,
|
||||
bytes32 poolId,
|
||||
address receiver,
|
||||
bool needsApproval
|
||||
bool needsApproval,
|
||||
TransferType transferType
|
||||
)
|
||||
{
|
||||
return _decodeData(data);
|
||||
}
|
||||
}
|
||||
|
||||
contract BalancerV2ExecutorTest is
|
||||
BalancerV2ExecutorExposed,
|
||||
Test,
|
||||
Constants
|
||||
{
|
||||
contract BalancerV2ExecutorTest is Constants, TestUtils {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
BalancerV2ExecutorExposed balancerV2Exposed;
|
||||
@@ -37,12 +36,17 @@ contract BalancerV2ExecutorTest is
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 17323404;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
balancerV2Exposed = new BalancerV2ExecutorExposed();
|
||||
balancerV2Exposed = new BalancerV2ExecutorExposed(PERMIT2_ADDRESS);
|
||||
}
|
||||
|
||||
function testDecodeParams() public view {
|
||||
bytes memory params = abi.encodePacked(
|
||||
WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, address(2), true
|
||||
WETH_ADDR,
|
||||
BAL_ADDR,
|
||||
WETH_BAL_POOL_ID,
|
||||
address(2),
|
||||
true,
|
||||
TokenTransfer.TransferType.NONE
|
||||
);
|
||||
|
||||
(
|
||||
@@ -50,7 +54,8 @@ contract BalancerV2ExecutorTest is
|
||||
IERC20 tokenOut,
|
||||
bytes32 poolId,
|
||||
address receiver,
|
||||
bool needsApproval
|
||||
bool needsApproval,
|
||||
TokenTransfer.TransferType transferType
|
||||
) = balancerV2Exposed.decodeParams(params);
|
||||
|
||||
assertEq(address(tokenIn), WETH_ADDR);
|
||||
@@ -58,6 +63,7 @@ contract BalancerV2ExecutorTest is
|
||||
assertEq(poolId, WETH_BAL_POOL_ID);
|
||||
assertEq(receiver, address(2));
|
||||
assertEq(needsApproval, true);
|
||||
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
|
||||
}
|
||||
|
||||
function testDecodeParamsInvalidDataLength() public {
|
||||
@@ -70,8 +76,14 @@ contract BalancerV2ExecutorTest is
|
||||
|
||||
function testSwap() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
bytes memory protocolData =
|
||||
abi.encodePacked(WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, BOB, true);
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
BAL_ADDR,
|
||||
WETH_BAL_POOL_ID,
|
||||
BOB,
|
||||
true,
|
||||
TokenTransfer.TransferType.NONE
|
||||
);
|
||||
|
||||
deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
|
||||
uint256 balanceBefore = BAL.balanceOf(BOB);
|
||||
@@ -84,16 +96,15 @@ contract BalancerV2ExecutorTest is
|
||||
}
|
||||
|
||||
function testDecodeIntegration() public view {
|
||||
// Generated by the SwapEncoder - test_encode_balancer_v2
|
||||
bytes memory protocolData =
|
||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01";
|
||||
|
||||
loadCallDataFromFile("test_encode_balancer_v2");
|
||||
(
|
||||
IERC20 tokenIn,
|
||||
IERC20 tokenOut,
|
||||
bytes32 poolId,
|
||||
address receiver,
|
||||
bool needsApproval
|
||||
bool needsApproval,
|
||||
TokenTransfer.TransferType transferType
|
||||
) = balancerV2Exposed.decodeParams(protocolData);
|
||||
|
||||
assertEq(address(tokenIn), WETH_ADDR);
|
||||
@@ -101,12 +112,13 @@ contract BalancerV2ExecutorTest is
|
||||
assertEq(poolId, WETH_BAL_POOL_ID);
|
||||
assertEq(receiver, BOB);
|
||||
assertEq(needsApproval, true);
|
||||
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
|
||||
}
|
||||
|
||||
function testSwapIntegration() public {
|
||||
// Generated by the SwapEncoder - test_encode_balancer_v2
|
||||
bytes memory protocolData =
|
||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01";
|
||||
loadCallDataFromFile("test_encode_balancer_v2");
|
||||
|
||||
uint256 amountIn = 10 ** 18;
|
||||
deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
|
||||
|
||||
@@ -22,7 +22,9 @@ interface MetaRegistry {
|
||||
}
|
||||
|
||||
contract CurveExecutorExposed is CurveExecutor {
|
||||
constructor(address _nativeToken) CurveExecutor(_nativeToken) {}
|
||||
constructor(address _nativeToken, address _permit2)
|
||||
CurveExecutor(_nativeToken, _permit2)
|
||||
{}
|
||||
|
||||
function decodeData(bytes calldata data)
|
||||
external
|
||||
@@ -34,7 +36,9 @@ contract CurveExecutorExposed is CurveExecutor {
|
||||
uint8 poolType,
|
||||
int128 i,
|
||||
int128 j,
|
||||
bool tokenApprovalNeeded
|
||||
bool tokenApprovalNeeded,
|
||||
TokenTransfer.TransferType transferType,
|
||||
address receiver
|
||||
)
|
||||
{
|
||||
return _decodeData(data);
|
||||
@@ -50,7 +54,8 @@ contract CurveExecutorTest is Test, Constants {
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 22031795;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
curveExecutorExposed = new CurveExecutorExposed(ETH_ADDR_FOR_CURVE);
|
||||
curveExecutorExposed =
|
||||
new CurveExecutorExposed(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
|
||||
metaRegistry = MetaRegistry(CURVE_META_REGISTRY);
|
||||
}
|
||||
|
||||
@@ -62,7 +67,9 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint8(3),
|
||||
uint8(2),
|
||||
uint8(0),
|
||||
true
|
||||
true,
|
||||
TokenTransfer.TransferType.NONE,
|
||||
ALICE
|
||||
);
|
||||
|
||||
(
|
||||
@@ -72,7 +79,9 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint8 poolType,
|
||||
int128 i,
|
||||
int128 j,
|
||||
bool tokenApprovalNeeded
|
||||
bool tokenApprovalNeeded,
|
||||
TokenTransfer.TransferType transferType,
|
||||
address receiver
|
||||
) = curveExecutorExposed.decodeData(data);
|
||||
|
||||
assertEq(tokenIn, WETH_ADDR);
|
||||
@@ -82,6 +91,8 @@ contract CurveExecutorTest is Test, Constants {
|
||||
assertEq(i, 2);
|
||||
assertEq(j, 0);
|
||||
assertEq(tokenApprovalNeeded, true);
|
||||
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
|
||||
assertEq(receiver, ALICE);
|
||||
}
|
||||
|
||||
function testTriPool() public {
|
||||
@@ -89,15 +100,12 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(DAI_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(DAI_ADDR, USDC_ADDR, TRIPOOL, 1);
|
||||
bytes memory data = _getData(DAI_ADDR, USDC_ADDR, TRIPOOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 999797);
|
||||
assertEq(
|
||||
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testStEthPool() public {
|
||||
@@ -106,14 +114,14 @@ contract CurveExecutorTest is Test, Constants {
|
||||
deal(address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data =
|
||||
_getData(ETH_ADDR_FOR_CURVE, STETH_ADDR, STETH_POOL, 1);
|
||||
_getData(ETH_ADDR_FOR_CURVE, STETH_ADDR, STETH_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 1001072414418410897);
|
||||
assertEq(
|
||||
IERC20(STETH_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
IERC20(STETH_ADDR).balanceOf(ALICE),
|
||||
amountOut - 1 // there is something weird in this pool, but won't investigate for now because we don't currently support it in the simulation
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,15 +130,13 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL, 3);
|
||||
bytes memory data =
|
||||
_getData(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL, 3, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 2279618);
|
||||
assertEq(
|
||||
IERC20(WBTC_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testSUSDPool() public {
|
||||
@@ -138,15 +144,12 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 100 * 10 ** 6;
|
||||
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(USDC_ADDR, SUSD_ADDR, SUSD_POOL, 1);
|
||||
bytes memory data = _getData(USDC_ADDR, SUSD_ADDR, SUSD_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 100488101605550214590);
|
||||
assertEq(
|
||||
IERC20(SUSD_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(SUSD_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testFraxUsdcPool() public {
|
||||
@@ -154,15 +157,13 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(FRAX_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL, 1);
|
||||
bytes memory data =
|
||||
_getData(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 998097);
|
||||
assertEq(
|
||||
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testUsdeUsdcPool() public {
|
||||
@@ -170,15 +171,13 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 100 * 10 ** 6;
|
||||
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL, 1);
|
||||
bytes memory data =
|
||||
_getData(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 100064812138999986170);
|
||||
assertEq(
|
||||
IERC20(USDE_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(USDE_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testDolaFraxPyusdPool() public {
|
||||
@@ -187,32 +186,27 @@ contract CurveExecutorTest is Test, Constants {
|
||||
deal(DOLA_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data =
|
||||
_getData(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL, 1);
|
||||
_getData(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 99688992);
|
||||
assertEq(
|
||||
IERC20(FRAXPYUSD_POOL).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(FRAXPYUSD_POOL).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testCryptoPoolWithETH() public {
|
||||
// Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
|
||||
uint256 amountIn = 1 ether;
|
||||
uint256 initialBalance = address(curveExecutorExposed).balance; // this address already has some ETH assigned to it
|
||||
uint256 initialBalance = address(ALICE).balance; // this address already has some ETH assigned to it
|
||||
deal(XYO_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data =
|
||||
_getData(XYO_ADDR, ETH_ADDR_FOR_CURVE, ETH_XYO_POOL, 2);
|
||||
_getData(XYO_ADDR, ETH_ADDR_FOR_CURVE, ETH_XYO_POOL, 2, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 6081816039338);
|
||||
assertEq(
|
||||
address(curveExecutorExposed).balance, initialBalance + amountOut
|
||||
);
|
||||
assertEq(ALICE.balance, initialBalance + amountOut);
|
||||
}
|
||||
|
||||
function testCryptoPool() public {
|
||||
@@ -220,15 +214,13 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 1000 ether;
|
||||
deal(BSGG_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL, 2);
|
||||
bytes memory data =
|
||||
_getData(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL, 2, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 23429);
|
||||
assertEq(
|
||||
IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testTricryptoPool() public {
|
||||
@@ -236,15 +228,13 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL, 2);
|
||||
bytes memory data =
|
||||
_getData(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL, 2, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 1861130974);
|
||||
assertEq(
|
||||
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testTwoCryptoPool() public {
|
||||
@@ -252,32 +242,27 @@ contract CurveExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(UWU_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data = _getData(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL, 2);
|
||||
bytes memory data =
|
||||
_getData(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL, 2, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 2873786684675);
|
||||
assertEq(
|
||||
IERC20(WETH_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testStableSwapPool() public {
|
||||
// Swapping CRVUSD -> USDT on a StableSwap pool, deployed by factory 0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d (plain pool)
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(CRVUSD_ADDR, address(curveExecutorExposed), amountIn);
|
||||
deal(USDT_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data =
|
||||
_getData(CRVUSD_ADDR, USDT_ADDR, CRVUSD_USDT_POOL, 1);
|
||||
_getData(USDT_ADDR, CRVUSD_ADDR, CRVUSD_USDT_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 999910);
|
||||
assertEq(
|
||||
IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(amountOut, 10436946786333182306400100);
|
||||
assertEq(IERC20(CRVUSD_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function testMetaPool() public {
|
||||
@@ -286,22 +271,20 @@ contract CurveExecutorTest is Test, Constants {
|
||||
deal(WTAO_ADDR, address(curveExecutorExposed), amountIn);
|
||||
|
||||
bytes memory data =
|
||||
_getData(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL, 1);
|
||||
_getData(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL, 1, ALICE);
|
||||
|
||||
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
|
||||
|
||||
assertEq(amountOut, 32797923610);
|
||||
assertEq(
|
||||
IERC20(WSTTAO_ADDR).balanceOf(address(curveExecutorExposed)),
|
||||
amountOut
|
||||
);
|
||||
assertEq(IERC20(WSTTAO_ADDR).balanceOf(ALICE), amountOut);
|
||||
}
|
||||
|
||||
function _getData(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
address pool,
|
||||
uint8 poolType
|
||||
uint8 poolType,
|
||||
address receiver
|
||||
) internal view returns (bytes memory data) {
|
||||
(int128 i, int128 j) = _getIndexes(tokenIn, tokenOut, pool);
|
||||
data = abi.encodePacked(
|
||||
@@ -311,7 +294,9 @@ contract CurveExecutorTest is Test, Constants {
|
||||
poolType,
|
||||
uint8(uint256(uint128(i))),
|
||||
uint8(uint256(uint128(j))),
|
||||
true
|
||||
true,
|
||||
TokenTransfer.TransferType.NONE,
|
||||
receiver
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {EkuboExecutor} from "@src/executors/EkuboExecutor.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "../TestUtils.sol";
|
||||
import {Constants} from "../Constants.sol";
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
|
||||
import {EkuboExecutor, TokenTransfer} from "@src/executors/EkuboExecutor.sol";
|
||||
import {ICore} from "@ekubo/interfaces/ICore.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
|
||||
import {console} from "forge-std/Test.sol";
|
||||
|
||||
contract EkuboExecutorTest is Test, Constants {
|
||||
contract EkuboExecutorTest is Constants, TestUtils {
|
||||
address constant EXECUTOR_ADDRESS =
|
||||
0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests
|
||||
EkuboExecutor executor;
|
||||
@@ -26,7 +27,7 @@ contract EkuboExecutorTest is Test, Constants {
|
||||
|
||||
deployCodeTo(
|
||||
"executors/EkuboExecutor.sol",
|
||||
abi.encode(CORE_ADDRESS),
|
||||
abi.encode(CORE_ADDRESS, PERMIT2_ADDRESS),
|
||||
EXECUTOR_ADDRESS
|
||||
);
|
||||
executor = EkuboExecutor(payable(EXECUTOR_ADDRESS));
|
||||
@@ -44,6 +45,7 @@ contract EkuboExecutorTest is Test, Constants {
|
||||
uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor));
|
||||
|
||||
bytes memory data = abi.encodePacked(
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType (transfer from executor to core)
|
||||
address(executor), // receiver
|
||||
NATIVE_TOKEN_ADDRESS, // tokenIn
|
||||
USDC_ADDR, // tokenOut
|
||||
@@ -80,6 +82,7 @@ contract EkuboExecutorTest is Test, Constants {
|
||||
uint256 ethBalanceBeforeExecutor = address(executor).balance;
|
||||
|
||||
bytes memory data = abi.encodePacked(
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType (transfer from executor to core)
|
||||
address(executor), // receiver
|
||||
USDC_ADDR, // tokenIn
|
||||
NATIVE_TOKEN_ADDRESS, // tokenOut
|
||||
@@ -137,6 +140,7 @@ contract EkuboExecutorTest is Test, Constants {
|
||||
// Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi
|
||||
function testMultiHopSwap() public {
|
||||
bytes memory data = abi.encodePacked(
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType
|
||||
address(executor), // receiver
|
||||
NATIVE_TOKEN_ADDRESS, // tokenIn
|
||||
USDC_ADDR, // tokenOut of 1st swap
|
||||
@@ -151,8 +155,6 @@ contract EkuboExecutorTest is Test, Constants {
|
||||
|
||||
// Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi
|
||||
function testMultiHopSwapIntegration() public {
|
||||
multiHopSwap(
|
||||
hex"ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032"
|
||||
);
|
||||
multiHopSwap(loadCallDataFromFile("test_ekubo_encode_swap_multi"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@src/executors/UniswapV2Executor.sol";
|
||||
import "@src/executors/TokenTransfer.sol";
|
||||
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||
import {Constants} from "../Constants.sol";
|
||||
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
|
||||
|
||||
contract UniswapV2ExecutorExposed is UniswapV2Executor {
|
||||
constructor(address _factory, bytes32 _initCode)
|
||||
UniswapV2Executor(_factory, _initCode)
|
||||
{}
|
||||
constructor(
|
||||
address _factory,
|
||||
bytes32 _initCode,
|
||||
address _permit2,
|
||||
uint256 _feeBps
|
||||
) UniswapV2Executor(_factory, _initCode, _permit2, _feeBps) {}
|
||||
|
||||
function decodeParams(bytes calldata data)
|
||||
external
|
||||
@@ -17,7 +22,8 @@ contract UniswapV2ExecutorExposed is UniswapV2Executor {
|
||||
IERC20 inToken,
|
||||
address target,
|
||||
address receiver,
|
||||
bool zeroForOne
|
||||
bool zeroForOne,
|
||||
TransferType transferType
|
||||
)
|
||||
{
|
||||
return _decodeData(data);
|
||||
@@ -46,7 +52,7 @@ contract FakeUniswapV2Pool {
|
||||
}
|
||||
}
|
||||
|
||||
contract UniswapV2ExecutorTest is Test, Constants {
|
||||
contract UniswapV2ExecutorTest is Test, Constants, Permit2TestHelper {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
UniswapV2ExecutorExposed uniswapV2Exposed;
|
||||
@@ -54,32 +60,54 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
||||
UniswapV2ExecutorExposed pancakeswapV2Exposed;
|
||||
IERC20 WETH = IERC20(WETH_ADDR);
|
||||
IERC20 DAI = IERC20(DAI_ADDR);
|
||||
IAllowanceTransfer permit2;
|
||||
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 17323404;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
uniswapV2Exposed = new UniswapV2ExecutorExposed(
|
||||
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH
|
||||
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS, 30
|
||||
);
|
||||
sushiswapV2Exposed = new UniswapV2ExecutorExposed(
|
||||
SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH
|
||||
SUSHISWAPV2_FACTORY_ETHEREUM,
|
||||
SUSHIV2_POOL_CODE_INIT_HASH,
|
||||
PERMIT2_ADDRESS,
|
||||
30
|
||||
);
|
||||
pancakeswapV2Exposed = new UniswapV2ExecutorExposed(
|
||||
PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH
|
||||
PANCAKESWAPV2_FACTORY_ETHEREUM,
|
||||
PANCAKEV2_POOL_CODE_INIT_HASH,
|
||||
PERMIT2_ADDRESS,
|
||||
25
|
||||
);
|
||||
permit2 = IAllowanceTransfer(PERMIT2_ADDRESS);
|
||||
}
|
||||
|
||||
function testDecodeParams() public view {
|
||||
bytes memory params =
|
||||
abi.encodePacked(WETH_ADDR, address(2), address(3), false);
|
||||
bytes memory params = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
address(2),
|
||||
address(3),
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
(IERC20 tokenIn, address target, address receiver, bool zeroForOne) =
|
||||
uniswapV2Exposed.decodeParams(params);
|
||||
(
|
||||
IERC20 tokenIn,
|
||||
address target,
|
||||
address receiver,
|
||||
bool zeroForOne,
|
||||
TokenTransfer.TransferType transferType
|
||||
) = uniswapV2Exposed.decodeParams(params);
|
||||
|
||||
assertEq(address(tokenIn), WETH_ADDR);
|
||||
assertEq(target, address(2));
|
||||
assertEq(receiver, address(3));
|
||||
assertEq(zeroForOne, false);
|
||||
assertEq(
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL),
|
||||
uint8(transferType)
|
||||
);
|
||||
}
|
||||
|
||||
function testDecodeParamsInvalidDataLength() public {
|
||||
@@ -126,12 +154,17 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
||||
assertGe(amountOut, 0);
|
||||
}
|
||||
|
||||
function testSwap() public {
|
||||
function testSwapWithTransfer() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
uint256 amountOut = 1847751195973566072891;
|
||||
bool zeroForOne = false;
|
||||
bytes memory protocolData =
|
||||
abi.encodePacked(WETH_ADDR, WETH_DAI_POOL, BOB, zeroForOne);
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
BOB,
|
||||
zeroForOne,
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
|
||||
);
|
||||
|
||||
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
||||
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||
@@ -140,24 +173,102 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
||||
assertGe(finalBalance, amountOut);
|
||||
}
|
||||
|
||||
function testDecodeIntegration() public view {
|
||||
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
|
||||
bytes memory protocolData =
|
||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100";
|
||||
function testSwapWithTransferFrom() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
uint256 amountOut = 1847751195973566072891;
|
||||
bool zeroForOne = false;
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
BOB,
|
||||
zeroForOne,
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL)
|
||||
);
|
||||
|
||||
(IERC20 tokenIn, address target, address receiver, bool zeroForOne) =
|
||||
uniswapV2Exposed.decodeParams(protocolData);
|
||||
deal(WETH_ADDR, address(this), amountIn);
|
||||
IERC20(WETH_ADDR).approve(address(uniswapV2Exposed), amountIn);
|
||||
|
||||
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||
|
||||
uint256 finalBalance = DAI.balanceOf(BOB);
|
||||
assertGe(finalBalance, amountOut);
|
||||
}
|
||||
|
||||
function testSwapWithPermit2TransferFrom() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
uint256 amountOut = 1847751195973566072891;
|
||||
bool zeroForOne = false;
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
ALICE,
|
||||
zeroForOne,
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL)
|
||||
);
|
||||
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
vm.startPrank(ALICE);
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(
|
||||
WETH_ADDR, address(uniswapV2Exposed), amountIn
|
||||
);
|
||||
|
||||
// Assume the permit2.approve method will be called from the TychoRouter
|
||||
// Replicate this scenario in this test.
|
||||
permit2.permit(ALICE, permitSingle, signature);
|
||||
|
||||
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 finalBalance = DAI.balanceOf(ALICE);
|
||||
assertGe(finalBalance, amountOut);
|
||||
}
|
||||
|
||||
function testSwapNoTransfer() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
uint256 amountOut = 1847751195973566072891;
|
||||
bool zeroForOne = false;
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
WETH_DAI_POOL,
|
||||
BOB,
|
||||
zeroForOne,
|
||||
uint8(TokenTransfer.TransferType.NONE)
|
||||
);
|
||||
|
||||
deal(WETH_ADDR, address(this), amountIn);
|
||||
IERC20(WETH_ADDR).transfer(address(WETH_DAI_POOL), amountIn);
|
||||
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||
|
||||
uint256 finalBalance = DAI.balanceOf(BOB);
|
||||
assertGe(finalBalance, amountOut);
|
||||
}
|
||||
|
||||
function testDecodeIntegration() public view {
|
||||
bytes memory protocolData =
|
||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f564000000000000000000000000000000000000000010000";
|
||||
|
||||
(
|
||||
IERC20 tokenIn,
|
||||
address target,
|
||||
address receiver,
|
||||
bool zeroForOne,
|
||||
TokenTransfer.TransferType transferType
|
||||
) = uniswapV2Exposed.decodeParams(protocolData);
|
||||
|
||||
assertEq(address(tokenIn), WETH_ADDR);
|
||||
assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
|
||||
assertEq(receiver, 0x0000000000000000000000000000000000000001);
|
||||
assertEq(zeroForOne, false);
|
||||
// TRANSFER = 0
|
||||
assertEq(0, uint8(transferType));
|
||||
}
|
||||
|
||||
function testSwapIntegration() public {
|
||||
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
|
||||
bytes memory protocolData =
|
||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00";
|
||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000";
|
||||
uint256 amountIn = 10 ** 18;
|
||||
uint256 amountOut = 1847751195973566072891;
|
||||
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
||||
@@ -171,8 +282,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
bool zeroForOne = false;
|
||||
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
|
||||
bytes memory protocolData =
|
||||
abi.encodePacked(WETH_ADDR, fakePool, BOB, zeroForOne);
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
fakePool,
|
||||
BOB,
|
||||
zeroForOne,
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
|
||||
);
|
||||
|
||||
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
||||
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
|
||||
@@ -186,8 +302,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
||||
vm.rollFork(26857267);
|
||||
uint256 amountIn = 10 * 10 ** 6;
|
||||
bool zeroForOne = true;
|
||||
bytes memory protocolData =
|
||||
abi.encodePacked(BASE_USDC, USDC_MAG7_POOL, BOB, zeroForOne);
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
BASE_USDC,
|
||||
USDC_MAG7_POOL,
|
||||
BOB,
|
||||
zeroForOne,
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
|
||||
);
|
||||
|
||||
deal(BASE_USDC, address(uniswapV2Exposed), amountIn);
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@src/executors/UniswapV3Executor.sol";
|
||||
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
|
||||
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||
import {Constants} from "../Constants.sol";
|
||||
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
|
||||
|
||||
contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
||||
constructor(address _factory, bytes32 _initCode)
|
||||
UniswapV3Executor(_factory, _initCode)
|
||||
constructor(address _factory, bytes32 _initCode, address _permit2)
|
||||
UniswapV3Executor(_factory, _initCode, _permit2)
|
||||
{}
|
||||
|
||||
function decodeData(bytes calldata data)
|
||||
@@ -19,7 +21,8 @@ contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
bool zeroForOne,
|
||||
TransferType transferType
|
||||
)
|
||||
{
|
||||
return _decodeData(data);
|
||||
@@ -35,30 +38,40 @@ contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
||||
}
|
||||
}
|
||||
|
||||
contract UniswapV3ExecutorTest is Test, Constants {
|
||||
contract UniswapV3ExecutorTest is Test, Constants, Permit2TestHelper {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
UniswapV3ExecutorExposed uniswapV3Exposed;
|
||||
UniswapV3ExecutorExposed pancakeV3Exposed;
|
||||
IERC20 WETH = IERC20(WETH_ADDR);
|
||||
IERC20 DAI = IERC20(DAI_ADDR);
|
||||
IAllowanceTransfer permit2;
|
||||
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 17323404;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
|
||||
uniswapV3Exposed = new UniswapV3ExecutorExposed(
|
||||
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH
|
||||
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
||||
);
|
||||
pancakeV3Exposed = new UniswapV3ExecutorExposed(
|
||||
PANCAKESWAPV3_DEPLOYER_ETHEREUM, PANCAKEV3_POOL_CODE_INIT_HASH
|
||||
PANCAKESWAPV3_DEPLOYER_ETHEREUM,
|
||||
PANCAKEV3_POOL_CODE_INIT_HASH,
|
||||
PERMIT2_ADDRESS
|
||||
);
|
||||
permit2 = IAllowanceTransfer(PERMIT2_ADDRESS);
|
||||
}
|
||||
|
||||
function testDecodeParams() public view {
|
||||
uint24 expectedPoolFee = 500;
|
||||
bytes memory data = abi.encodePacked(
|
||||
WETH_ADDR, DAI_ADDR, expectedPoolFee, address(2), address(3), false
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
expectedPoolFee,
|
||||
address(2),
|
||||
address(3),
|
||||
false,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
(
|
||||
@@ -67,7 +80,8 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
bool zeroForOne,
|
||||
TokenTransfer.TransferType transferType
|
||||
) = uniswapV3Exposed.decodeData(data);
|
||||
|
||||
assertEq(tokenIn, WETH_ADDR);
|
||||
@@ -76,6 +90,33 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
||||
assertEq(receiver, address(2));
|
||||
assertEq(target, address(3));
|
||||
assertEq(zeroForOne, false);
|
||||
assertEq(
|
||||
uint8(transferType),
|
||||
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
|
||||
);
|
||||
}
|
||||
|
||||
function testSwapIntegration() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
|
||||
|
||||
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
||||
bool zeroForOne = false;
|
||||
|
||||
bytes memory data = encodeUniswapV3Swap(
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
address(this),
|
||||
DAI_WETH_USV3,
|
||||
zeroForOne,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
uint256 amountOut = uniswapV3Exposed.swap(amountIn, data);
|
||||
|
||||
assertGe(amountOut, expAmountOut);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(address(uniswapV3Exposed)), 0);
|
||||
assertGe(IERC20(DAI_ADDR).balanceOf(address(this)), expAmountOut);
|
||||
}
|
||||
|
||||
function testDecodeParamsInvalidDataLength() public {
|
||||
@@ -105,12 +146,18 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
||||
uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
|
||||
|
||||
vm.startPrank(DAI_WETH_USV3);
|
||||
bytes memory protocolData =
|
||||
abi.encodePacked(WETH_ADDR, DAI_ADDR, poolFee);
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
poolFee,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
|
||||
address(uniswapV3Exposed)
|
||||
);
|
||||
uint256 dataOffset = 3; // some offset
|
||||
uint256 dataLength = protocolData.length;
|
||||
|
||||
bytes memory callbackData = abi.encodePacked(
|
||||
bytes4(0xfa461e33),
|
||||
int256(amountOwed), // amount0Delta
|
||||
int256(0), // amount1Delta
|
||||
dataOffset,
|
||||
@@ -124,24 +171,6 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
||||
assertEq(finalPoolReserve - initialPoolReserve, amountOwed);
|
||||
}
|
||||
|
||||
function testSwapIntegration() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
|
||||
|
||||
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
||||
bool zeroForOne = false;
|
||||
|
||||
bytes memory data = encodeUniswapV3Swap(
|
||||
WETH_ADDR, DAI_ADDR, address(this), DAI_WETH_USV3, zeroForOne
|
||||
);
|
||||
|
||||
uint256 amountOut = uniswapV3Exposed.swap(amountIn, data);
|
||||
|
||||
assertGe(amountOut, expAmountOut);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(address(uniswapV3Exposed)), 0);
|
||||
assertGe(IERC20(DAI_ADDR).balanceOf(address(this)), expAmountOut);
|
||||
}
|
||||
|
||||
function testSwapFailureInvalidTarget() public {
|
||||
uint256 amountIn = 10 ** 18;
|
||||
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
|
||||
@@ -154,7 +183,8 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
||||
uint24(3000),
|
||||
address(this),
|
||||
fakePool,
|
||||
zeroForOne
|
||||
zeroForOne,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
|
||||
);
|
||||
|
||||
vm.expectRevert(UniswapV3Executor__InvalidTarget.selector);
|
||||
@@ -166,11 +196,18 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
||||
address tokenOut,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zero2one
|
||||
bool zero2one,
|
||||
TokenTransfer.TransferType transferType
|
||||
) internal view returns (bytes memory) {
|
||||
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||
return abi.encodePacked(
|
||||
tokenIn, tokenOut, pool.fee(), receiver, target, zero2one
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
pool.fee(),
|
||||
receiver,
|
||||
target,
|
||||
zero2one,
|
||||
transferType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "../../src/executors/UniswapV4Executor.sol";
|
||||
import "../TestUtils.sol";
|
||||
import "./UniswapV4Utils.sol";
|
||||
import "@src/executors/TokenTransfer.sol";
|
||||
import "@src/executors/UniswapV4Executor.sol";
|
||||
import {Constants} from "../Constants.sol";
|
||||
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
|
||||
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||
|
||||
contract UniswapV4ExecutorExposed is UniswapV4Executor {
|
||||
constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {}
|
||||
constructor(IPoolManager _poolManager, address _permit2)
|
||||
UniswapV4Executor(_poolManager, _permit2)
|
||||
{}
|
||||
|
||||
function decodeData(bytes calldata data)
|
||||
external
|
||||
@@ -18,7 +22,8 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
bool zeroForOne,
|
||||
address callbackExecutor,
|
||||
TokenTransfer.TransferType transferType,
|
||||
address receiver,
|
||||
UniswapV4Pool[] memory pools
|
||||
)
|
||||
{
|
||||
@@ -26,7 +31,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
|
||||
}
|
||||
}
|
||||
|
||||
contract UniswapV4ExecutorTest is Test, Constants {
|
||||
contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
UniswapV4ExecutorExposed uniswapV4Exposed;
|
||||
@@ -37,8 +42,9 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 21817316;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
uniswapV4Exposed =
|
||||
new UniswapV4ExecutorExposed(IPoolManager(poolManager));
|
||||
uniswapV4Exposed = new UniswapV4ExecutorExposed(
|
||||
IPoolManager(poolManager), PERMIT2_ADDRESS
|
||||
);
|
||||
}
|
||||
|
||||
function testDecodeParams() public view {
|
||||
@@ -47,6 +53,8 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
int24 tickSpacing1 = 60;
|
||||
uint24 pool2Fee = 1000;
|
||||
int24 tickSpacing2 = -10;
|
||||
TokenTransfer.TransferType transferType =
|
||||
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL;
|
||||
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools =
|
||||
new UniswapV4Executor.UniswapV4Pool[](2);
|
||||
@@ -62,21 +70,23 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
});
|
||||
|
||||
bytes memory data = UniswapV4Utils.encodeExactInput(
|
||||
USDE_ADDR, USDT_ADDR, zeroForOne, address(uniswapV4Exposed), pools
|
||||
USDE_ADDR, USDT_ADDR, zeroForOne, transferType, ALICE, pools
|
||||
);
|
||||
|
||||
(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
bool zeroForOneDecoded,
|
||||
address callbackExecutor,
|
||||
TokenTransfer.TransferType transferTypeDecoded,
|
||||
address receiver,
|
||||
UniswapV4Executor.UniswapV4Pool[] memory decodedPools
|
||||
) = uniswapV4Exposed.decodeData(data);
|
||||
|
||||
assertEq(tokenIn, USDE_ADDR);
|
||||
assertEq(tokenOut, USDT_ADDR);
|
||||
assertEq(zeroForOneDecoded, zeroForOne);
|
||||
assertEq(callbackExecutor, address(uniswapV4Exposed));
|
||||
assertEq(uint8(transferTypeDecoded), uint8(transferType));
|
||||
assertEq(receiver, ALICE);
|
||||
assertEq(decodedPools.length, 2);
|
||||
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
|
||||
assertEq(decodedPools[0].fee, pool1Fee);
|
||||
@@ -102,7 +112,12 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
});
|
||||
|
||||
bytes memory data = UniswapV4Utils.encodeExactInput(
|
||||
USDE_ADDR, USDT_ADDR, true, address(uniswapV4Exposed), pools
|
||||
USDE_ADDR,
|
||||
USDT_ADDR,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
|
||||
ALICE,
|
||||
pools
|
||||
);
|
||||
|
||||
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
|
||||
@@ -111,14 +126,13 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
USDE.balanceOf(address(uniswapV4Exposed)),
|
||||
usdeBalanceBeforeSwapExecutor - amountIn
|
||||
);
|
||||
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut);
|
||||
assertTrue(USDT.balanceOf(ALICE) == amountOut);
|
||||
}
|
||||
|
||||
function testSingleSwapIntegration() public {
|
||||
// USDE -> USDT
|
||||
// Generated by the Tycho swap encoder - test_encode_uniswap_v4_simple_swap
|
||||
bytes memory protocolData =
|
||||
hex"4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec701f62849f9a0b5bf2913b396098f7c7019b51a820adac17f958d2ee523a2206206994597c13d831ec7000064000001";
|
||||
loadCallDataFromFile("test_encode_uniswap_v4_simple_swap");
|
||||
uint256 amountIn = 100 ether;
|
||||
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
|
||||
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
|
||||
@@ -128,10 +142,9 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
|
||||
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
|
||||
assertEq(
|
||||
USDE.balanceOf(address(uniswapV4Exposed)),
|
||||
usdeBalanceBeforeSwapExecutor - amountIn
|
||||
USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn
|
||||
);
|
||||
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut);
|
||||
assertTrue(USDT.balanceOf(ALICE) == amountOut);
|
||||
}
|
||||
|
||||
function testMultipleSwap() public {
|
||||
@@ -156,7 +169,12 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
});
|
||||
|
||||
bytes memory data = UniswapV4Utils.encodeExactInput(
|
||||
USDE_ADDR, WBTC_ADDR, true, address(uniswapV4Exposed), pools
|
||||
USDE_ADDR,
|
||||
WBTC_ADDR,
|
||||
true,
|
||||
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
|
||||
ALICE,
|
||||
pools
|
||||
);
|
||||
|
||||
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
|
||||
@@ -165,17 +183,13 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
USDE.balanceOf(address(uniswapV4Exposed)),
|
||||
usdeBalanceBeforeSwapExecutor - amountIn
|
||||
);
|
||||
assertTrue(
|
||||
IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut
|
||||
);
|
||||
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
|
||||
}
|
||||
|
||||
function testMultipleSwapIntegration() public {
|
||||
// USDE -> USDT -> WBTC
|
||||
// Generated by the Tycho swap encoder - test_encode_uniswap_v4_sequential_swap
|
||||
|
||||
bytes memory protocolData =
|
||||
hex"4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c59901f62849f9a0b5bf2913b396098f7c7019b51a820adac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c";
|
||||
loadCallDataFromFile("test_encode_uniswap_v4_sequential_swap");
|
||||
|
||||
uint256 amountIn = 100 ether;
|
||||
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
|
||||
@@ -189,8 +203,6 @@ contract UniswapV4ExecutorTest is Test, Constants {
|
||||
USDE.balanceOf(address(uniswapV4Exposed)),
|
||||
usdeBalanceBeforeSwapExecutor - amountIn
|
||||
);
|
||||
assertTrue(
|
||||
IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut
|
||||
);
|
||||
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ library UniswapV4Utils {
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
bool zeroForOne,
|
||||
address callbackExecutor,
|
||||
UniswapV4Executor.TransferType transferType,
|
||||
address receiver,
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools
|
||||
) public pure returns (bytes memory) {
|
||||
bytes memory encodedPools;
|
||||
@@ -23,7 +24,7 @@ library UniswapV4Utils {
|
||||
}
|
||||
|
||||
return abi.encodePacked(
|
||||
tokenIn, tokenOut, zeroForOne, callbackExecutor, encodedPools
|
||||
tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::io::{self, Read};
|
||||
use clap::{Parser, Subcommand};
|
||||
use tycho_common::{hex_bytes::Bytes, models::Chain};
|
||||
use tycho_execution::encoding::{
|
||||
evm::encoder_builder::EVMEncoderBuilder, models::Solution, tycho_encoder::TychoEncoder,
|
||||
evm::encoder_builders::{TychoExecutorEncoderBuilder, TychoRouterEncoderBuilder},
|
||||
models::Solution,
|
||||
tycho_encoder::TychoEncoder,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -42,57 +44,62 @@ pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
#[arg(short, long)]
|
||||
chain: Chain,
|
||||
#[arg(short, long)]
|
||||
executors_file_path: Option<String>,
|
||||
#[arg(short, long)]
|
||||
router_address: Option<Bytes>,
|
||||
#[arg(short, long)]
|
||||
swapper_pk: Option<String>,
|
||||
#[arg(short, long)]
|
||||
token_in_already_in_router: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Use the Tycho router encoding strategy
|
||||
/// Use Tycho router encoding
|
||||
TychoRouter,
|
||||
/// Use the Tycho router encoding strategy with Permit2 approval and token in transfer
|
||||
TychoRouterPermit2 {
|
||||
#[arg(short, long)]
|
||||
swapper_pk: String,
|
||||
},
|
||||
/// Use the direct execution encoding strategy
|
||||
DirectExecution,
|
||||
/// Use direct execution encoding
|
||||
TychoExecutor,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
let chain = Chain::Ethereum;
|
||||
|
||||
// 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))?;
|
||||
.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());
|
||||
}
|
||||
let solution: Solution = serde_json::from_str(&buffer)?;
|
||||
|
||||
let mut builder = EVMEncoderBuilder::new().chain(chain);
|
||||
|
||||
let chain = cli.chain;
|
||||
let encoder: Box<dyn TychoEncoder> = match cli.command {
|
||||
Commands::TychoRouter => {
|
||||
let mut builder = TychoRouterEncoderBuilder::new().chain(chain);
|
||||
if let Some(config_path) = cli.executors_file_path {
|
||||
builder = builder.executors_file_path(config_path);
|
||||
}
|
||||
if let Some(router_address) = cli.router_address {
|
||||
builder = builder.router_address(router_address);
|
||||
}
|
||||
|
||||
builder = match cli.command {
|
||||
Commands::TychoRouter => builder.initialize_tycho_router()?,
|
||||
Commands::TychoRouterPermit2 { swapper_pk } => {
|
||||
builder.initialize_tycho_router_with_permit2(swapper_pk)?
|
||||
if let Some(swapper_pk) = cli.swapper_pk {
|
||||
builder = builder.swapper_pk(swapper_pk);
|
||||
}
|
||||
Commands::DirectExecution => builder.initialize_direct_execution()?,
|
||||
if let Some(token_in_already_in_router) = cli.token_in_already_in_router {
|
||||
builder = builder.token_in_already_in_router(token_in_already_in_router);
|
||||
}
|
||||
builder.build()?
|
||||
}
|
||||
Commands::TychoExecutor => TychoExecutorEncoderBuilder::new()
|
||||
.chain(chain)
|
||||
.build()?,
|
||||
};
|
||||
let encoder = builder.build()?;
|
||||
let transactions = encoder.encode_router_calldata(vec![solution])?;
|
||||
|
||||
let transactions = encoder.encode_calldata(vec![solution])?;
|
||||
let encoded = serde_json::json!({
|
||||
"to": format!("0x{}", hex::encode(&transactions[0].to)),
|
||||
"value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())),
|
||||
@@ -101,8 +108,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Output the encoded result as JSON to stdout
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&encoded)
|
||||
.map_err(|e| format!("Failed to serialize output: {}", e))?
|
||||
serde_json::to_string(&encoded).map_err(|e| format!("Failed to serialize output: {e}"))?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -116,8 +116,7 @@ impl Permit2 {
|
||||
Ok(allowance)
|
||||
}
|
||||
Err(err) => Err(EncodingError::RecoverableError(format!(
|
||||
"Call to permit2 allowance method failed with error: {:?}",
|
||||
err
|
||||
"Call to permit2 allowance method failed with error: {err}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
@@ -158,8 +157,7 @@ impl Permit2 {
|
||||
.sign_hash_sync(&hash)
|
||||
.map_err(|e| {
|
||||
EncodingError::FatalError(format!(
|
||||
"Failed to sign permit2 approval with error: {}",
|
||||
e
|
||||
"Failed to sign permit2 approval with error: {e}"
|
||||
))
|
||||
})?;
|
||||
Ok((permit_single, signature))
|
||||
@@ -172,7 +170,7 @@ mod tests {
|
||||
|
||||
use alloy_primitives::Uint;
|
||||
use num_bigint::BigUint;
|
||||
use tycho_common::models::Chain as TychoCoreChain;
|
||||
use tycho_common::models::Chain as TychoCommonChain;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -208,7 +206,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn eth_chain() -> Chain {
|
||||
TychoCoreChain::Ethereum.into()
|
||||
TychoCommonChain::Ethereum.into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -64,8 +64,7 @@ impl ProtocolApprovalsManager {
|
||||
Ok(allowance.is_zero())
|
||||
}
|
||||
Err(err) => Err(EncodingError::RecoverableError(format!(
|
||||
"Allowance call failed with error: {:?}",
|
||||
err
|
||||
"Allowance call failed with error: {err}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,36 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str =
|
||||
pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
let mut set = HashSet::new();
|
||||
set.insert("uniswap_v4");
|
||||
set.insert("balancer_v3");
|
||||
set.insert("vm:balancer_v3");
|
||||
set.insert("ekubo_v2");
|
||||
set
|
||||
});
|
||||
|
||||
/// These protocols need an external in transfer to the pool. This transfer can be from the router,
|
||||
/// from the user or from the previous pool. Any protocols that are not defined here expect funds to
|
||||
/// be in the router at the time of swap and do the transfer themselves from msg.sender
|
||||
pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
let mut set = HashSet::new();
|
||||
set.insert("uniswap_v2");
|
||||
set.insert("sushiswap_v2");
|
||||
set.insert("pancakeswap_v2");
|
||||
set.insert("uniswap_v3");
|
||||
set.insert("pancakeswap_v3");
|
||||
set.insert("uniswap_v4");
|
||||
set.insert("ekubo_v2");
|
||||
set
|
||||
});
|
||||
|
||||
// The protocols here are a subset of the ones defined in IN_TRANSFER_REQUIRED_PROTOCOLS. The tokens
|
||||
// can not be sent directly from the previous pool into a pool of this protocol. The tokens need to
|
||||
// be sent to the router and only then transferred into the pool. This is the case for uniswap v3
|
||||
// because of the callback logic. The only way for this to work it would be to call the second swap
|
||||
// during the callback of the first swap. This is currently not supported.
|
||||
pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
let mut set = HashSet::new();
|
||||
set.insert("uniswap_v3");
|
||||
set.insert("pancakeswap_v3");
|
||||
set.insert("uniswap_v4");
|
||||
set.insert("ekubo_v2");
|
||||
set
|
||||
});
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
use tycho_common::{models::Chain, Bytes};
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::{
|
||||
strategy_encoder::strategy_encoders::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder},
|
||||
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
|
||||
tycho_encoder::EVMTychoEncoder,
|
||||
},
|
||||
strategy_encoder::StrategyEncoder,
|
||||
};
|
||||
|
||||
/// Builder pattern for constructing an `EVMTychoEncoder` with customizable options.
|
||||
///
|
||||
/// This struct allows setting a chain and strategy encoder before building the final encoder.
|
||||
pub struct EVMEncoderBuilder {
|
||||
strategy: Option<Box<dyn StrategyEncoder>>,
|
||||
chain: Option<Chain>,
|
||||
executors_file_path: Option<String>,
|
||||
router_address: Option<Bytes>,
|
||||
}
|
||||
|
||||
impl Default for EVMEncoderBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EVMEncoderBuilder {
|
||||
pub fn new() -> Self {
|
||||
EVMEncoderBuilder {
|
||||
chain: None,
|
||||
strategy: None,
|
||||
executors_file_path: None,
|
||||
router_address: None,
|
||||
}
|
||||
}
|
||||
pub fn chain(mut self, chain: Chain) -> Self {
|
||||
self.chain = Some(chain);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `executors_file_path` manually.
|
||||
/// If it's not set, the default path will be used (config/executor_addresses.json)
|
||||
pub fn executors_file_path(mut self, executors_file_path: String) -> Self {
|
||||
self.executors_file_path = Some(executors_file_path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `router_address` manually.
|
||||
/// If it's not set, the default router address will be used (config/router_addresses.json)
|
||||
pub fn router_address(mut self, router_address: Bytes) -> Self {
|
||||
self.router_address = Some(router_address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `strategy_encoder` manually.
|
||||
///
|
||||
/// **Note**: This method should not be used in combination with `tycho_router` or
|
||||
/// `direct_execution`.
|
||||
pub fn strategy_encoder(mut self, strategy: Box<dyn StrategyEncoder>) -> Self {
|
||||
self.strategy = Some(strategy);
|
||||
self
|
||||
}
|
||||
|
||||
/// Shortcut method to initialize a `SplitSwapStrategyEncoder` without any approval nor token in
|
||||
/// transfer. **Note**: Should not be used at the same time as `strategy_encoder`.
|
||||
pub fn initialize_tycho_router(self) -> Result<Self, EncodingError> {
|
||||
if let Some(chain) = self.chain {
|
||||
let swap_encoder_registry =
|
||||
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?;
|
||||
let strategy = Box::new(SplitSwapStrategyEncoder::new(
|
||||
chain,
|
||||
swap_encoder_registry,
|
||||
None,
|
||||
self.router_address.clone(),
|
||||
)?);
|
||||
Ok(EVMEncoderBuilder {
|
||||
chain: Some(chain),
|
||||
strategy: Some(strategy),
|
||||
executors_file_path: self.executors_file_path,
|
||||
router_address: self.router_address,
|
||||
})
|
||||
} else {
|
||||
Err(EncodingError::FatalError(
|
||||
"Please set the chain before setting the tycho router".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortcut method to initialize a `SplitSwapStrategyEncoder` with Permit2 approval and token
|
||||
/// in transfer. **Note**: Should not be used at the same time as `strategy_encoder`.
|
||||
pub fn initialize_tycho_router_with_permit2(
|
||||
self,
|
||||
swapper_pk: String,
|
||||
) -> Result<Self, EncodingError> {
|
||||
if let Some(chain) = self.chain {
|
||||
let swap_encoder_registry =
|
||||
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?;
|
||||
let strategy = Box::new(SplitSwapStrategyEncoder::new(
|
||||
chain,
|
||||
swap_encoder_registry,
|
||||
Some(swapper_pk),
|
||||
self.router_address.clone(),
|
||||
)?);
|
||||
Ok(EVMEncoderBuilder {
|
||||
chain: Some(chain),
|
||||
strategy: Some(strategy),
|
||||
executors_file_path: self.executors_file_path,
|
||||
router_address: self.router_address,
|
||||
})
|
||||
} else {
|
||||
Err(EncodingError::FatalError(
|
||||
"Please set the chain before setting the tycho router".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortcut method to initialize an `ExecutorStrategyEncoder`.
|
||||
/// **Note**: Should not be used at the same time as `strategy_encoder`.
|
||||
pub fn initialize_direct_execution(self) -> Result<Self, EncodingError> {
|
||||
if let Some(chain) = self.chain {
|
||||
let swap_encoder_registry =
|
||||
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?;
|
||||
let strategy = Box::new(ExecutorStrategyEncoder::new(swap_encoder_registry));
|
||||
Ok(EVMEncoderBuilder {
|
||||
chain: Some(chain),
|
||||
strategy: Some(strategy),
|
||||
executors_file_path: self.executors_file_path,
|
||||
router_address: self.router_address,
|
||||
})
|
||||
} else {
|
||||
Err(EncodingError::FatalError(
|
||||
"Please set the chain before setting the strategy".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the `EVMTychoEncoder` instance using the configured chain and strategy.
|
||||
/// Returns an error if either the chain or strategy has not been set.
|
||||
pub fn build(self) -> Result<EVMTychoEncoder, EncodingError> {
|
||||
if let (Some(chain), Some(strategy)) = (self.chain, self.strategy) {
|
||||
EVMTychoEncoder::new(chain, strategy)
|
||||
} else {
|
||||
Err(EncodingError::FatalError(
|
||||
"Please set the chain and strategy before building the encoder".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
155
src/encoding/evm/encoder_builders.rs
Normal file
155
src/encoding/evm/encoder_builders.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tycho_common::{models::Chain as TychoCommonChain, Bytes};
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::{
|
||||
constants::DEFAULT_ROUTERS_JSON,
|
||||
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
|
||||
tycho_encoders::{TychoExecutorEncoder, TychoRouterEncoder},
|
||||
},
|
||||
models::Chain,
|
||||
tycho_encoder::TychoEncoder,
|
||||
};
|
||||
|
||||
/// Builder pattern for constructing a `TychoRouterEncoder` with customizable options.
|
||||
///
|
||||
/// This struct allows setting a chain and strategy encoder before building the final encoder.
|
||||
pub struct TychoRouterEncoderBuilder {
|
||||
swapper_pk: Option<String>,
|
||||
chain: Option<Chain>,
|
||||
executors_file_path: Option<String>,
|
||||
router_address: Option<Bytes>,
|
||||
token_in_already_in_router: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for TychoRouterEncoderBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TychoRouterEncoderBuilder {
|
||||
pub fn new() -> Self {
|
||||
TychoRouterEncoderBuilder {
|
||||
swapper_pk: None,
|
||||
chain: None,
|
||||
executors_file_path: None,
|
||||
router_address: None,
|
||||
token_in_already_in_router: None,
|
||||
}
|
||||
}
|
||||
pub fn chain(mut self, chain: TychoCommonChain) -> Self {
|
||||
self.chain = Some(chain.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `executors_file_path` manually.
|
||||
/// If it's not set, the default path will be used (config/executor_addresses.json)
|
||||
pub fn executors_file_path(mut self, executors_file_path: String) -> Self {
|
||||
self.executors_file_path = Some(executors_file_path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `router_address` manually.
|
||||
/// If it's not set, the default router address will be used (config/router_addresses.json)
|
||||
pub fn router_address(mut self, router_address: Bytes) -> Self {
|
||||
self.router_address = Some(router_address);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn swapper_pk(mut self, swapper_pk: String) -> Self {
|
||||
self.swapper_pk = Some(swapper_pk);
|
||||
self
|
||||
}
|
||||
|
||||
// Sets the `token_in_already_in_router` flag.
|
||||
// If set to true, the encoder will assume that the token in is already in the router.
|
||||
// WARNING: this is an advanced feature and should be used with caution. Make sure you have
|
||||
// checks to make sure that your tokens won't be lost. The Router is not considered safe to hold
|
||||
// tokens, so if this is not done within the same transaction you will lose your tokens.
|
||||
pub fn token_in_already_in_router(mut self, token_in_already_in_router: bool) -> Self {
|
||||
self.token_in_already_in_router = Some(token_in_already_in_router);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the `TychoRouterEncoder` instance using the configured chain.
|
||||
/// Returns an error if either the chain has not been set.
|
||||
pub fn build(self) -> Result<Box<dyn TychoEncoder>, EncodingError> {
|
||||
if let Some(chain) = self.chain {
|
||||
let tycho_router_address;
|
||||
if let Some(address) = self.router_address {
|
||||
tycho_router_address = address;
|
||||
} else {
|
||||
let default_routers: HashMap<String, Bytes> =
|
||||
serde_json::from_str(DEFAULT_ROUTERS_JSON)?;
|
||||
tycho_router_address = default_routers
|
||||
.get(&chain.name)
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"No default router address found for chain".to_string(),
|
||||
))?
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
let swap_encoder_registry =
|
||||
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain.clone())?;
|
||||
|
||||
Ok(Box::new(TychoRouterEncoder::new(
|
||||
chain,
|
||||
swap_encoder_registry,
|
||||
self.swapper_pk,
|
||||
tycho_router_address,
|
||||
self.token_in_already_in_router
|
||||
.unwrap_or(false),
|
||||
)?))
|
||||
} else {
|
||||
Err(EncodingError::FatalError(
|
||||
"Please set the chain and router address before building the encoder".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder pattern for constructing a `TychoExecutorEncoder` with customizable options.
|
||||
pub struct TychoExecutorEncoderBuilder {
|
||||
chain: Option<Chain>,
|
||||
executors_file_path: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for TychoExecutorEncoderBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TychoExecutorEncoderBuilder {
|
||||
pub fn new() -> Self {
|
||||
TychoExecutorEncoderBuilder { chain: None, executors_file_path: None }
|
||||
}
|
||||
pub fn chain(mut self, chain: TychoCommonChain) -> Self {
|
||||
self.chain = Some(chain.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `executors_file_path` manually.
|
||||
/// If it's not set, the default path will be used (config/executor_addresses.json)
|
||||
pub fn executors_file_path(mut self, executors_file_path: String) -> Self {
|
||||
self.executors_file_path = Some(executors_file_path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the `TychoExecutorEncoder` instance using the configured chain and strategy.
|
||||
/// Returns an error if either the chain or strategy has not been set.
|
||||
pub fn build(self) -> Result<Box<dyn TychoEncoder>, EncodingError> {
|
||||
if let Some(chain) = self.chain {
|
||||
let swap_encoder_registry =
|
||||
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain.clone())?;
|
||||
Ok(Box::new(TychoExecutorEncoder::new(chain, swap_encoder_registry)?))
|
||||
} else {
|
||||
Err(EncodingError::FatalError(
|
||||
"Please set the chain and strategy before building the encoder".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,15 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap};
|
||||
/// optimization.
|
||||
///
|
||||
/// # Fields
|
||||
/// * `input_token`: Bytes, the input token of the first swap
|
||||
/// * `output_token`: Bytes, the output token of the final swap
|
||||
/// * `token_in`: Bytes, the input token of the first swap
|
||||
/// * `token_out`: Bytes, the output token of the final swap
|
||||
/// * `protocol_system`: String, the protocol system of the swaps
|
||||
/// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group
|
||||
/// * `split`: f64, the split percentage of the first swap in the group
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct SwapGroup {
|
||||
pub input_token: Bytes,
|
||||
pub output_token: Bytes,
|
||||
pub token_in: Bytes,
|
||||
pub token_out: Bytes,
|
||||
pub protocol_system: String,
|
||||
pub swaps: Vec<Swap>,
|
||||
pub split: f64,
|
||||
@@ -44,7 +44,7 @@ pub fn group_swaps(swaps: Vec<Swap>) -> Vec<SwapGroup> {
|
||||
if let Some(group) = current_group.as_mut() {
|
||||
group.swaps.push(swap.clone());
|
||||
// Update the output token of the current group.
|
||||
group.output_token = swap.token_out.clone();
|
||||
group.token_out = swap.token_out.clone();
|
||||
}
|
||||
} else {
|
||||
// Not second or later USV4 pool. Push the current group (if it exists) and then
|
||||
@@ -53,8 +53,8 @@ pub fn group_swaps(swaps: Vec<Swap>) -> Vec<SwapGroup> {
|
||||
grouped_swaps.push(group.clone());
|
||||
}
|
||||
current_group = Some(SwapGroup {
|
||||
input_token: swap.token_in.clone(),
|
||||
output_token: swap.token_out.clone(),
|
||||
token_in: swap.token_in.clone(),
|
||||
token_out: swap.token_out.clone(),
|
||||
protocol_system: current_swap_protocol.clone(),
|
||||
swaps: vec![swap.clone()],
|
||||
split: swap.split,
|
||||
@@ -135,15 +135,15 @@ mod tests {
|
||||
vec![
|
||||
SwapGroup {
|
||||
swaps: vec![swap_weth_wbtc, swap_wbtc_usdc],
|
||||
input_token: weth,
|
||||
output_token: usdc.clone(),
|
||||
token_in: weth,
|
||||
token_out: usdc.clone(),
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
split: 0f64,
|
||||
},
|
||||
SwapGroup {
|
||||
swaps: vec![swap_usdc_dai],
|
||||
input_token: usdc,
|
||||
output_token: dai,
|
||||
token_in: usdc,
|
||||
token_out: dai,
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
split: 0f64,
|
||||
}
|
||||
@@ -216,22 +216,22 @@ mod tests {
|
||||
vec![
|
||||
SwapGroup {
|
||||
swaps: vec![swap_wbtc_weth],
|
||||
input_token: wbtc.clone(),
|
||||
output_token: weth.clone(),
|
||||
token_in: wbtc.clone(),
|
||||
token_out: weth.clone(),
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
split: 0f64,
|
||||
},
|
||||
SwapGroup {
|
||||
swaps: vec![swap_weth_usdc],
|
||||
input_token: weth.clone(),
|
||||
output_token: usdc.clone(),
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
split: 0.5f64,
|
||||
},
|
||||
SwapGroup {
|
||||
swaps: vec![swap_weth_dai, swap_dai_usdc],
|
||||
input_token: weth,
|
||||
output_token: usdc,
|
||||
token_in: weth,
|
||||
token_out: usdc,
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
split: 0f64,
|
||||
}
|
||||
@@ -255,7 +255,7 @@ mod tests {
|
||||
|
||||
let swap_weth_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "balancer_v3".to_string(),
|
||||
protocol_system: "vm:balancer_v3".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
@@ -264,7 +264,7 @@ mod tests {
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "balancer_v3".to_string(),
|
||||
protocol_system: "vm:balancer_v3".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
@@ -304,15 +304,15 @@ mod tests {
|
||||
vec![
|
||||
SwapGroup {
|
||||
swaps: vec![swap_weth_wbtc, swap_wbtc_usdc],
|
||||
input_token: weth.clone(),
|
||||
output_token: usdc.clone(),
|
||||
protocol_system: "balancer_v3".to_string(),
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
protocol_system: "vm:balancer_v3".to_string(),
|
||||
split: 0.5f64,
|
||||
},
|
||||
SwapGroup {
|
||||
swaps: vec![swap_weth_dai, swap_dai_usdc],
|
||||
input_token: weth,
|
||||
output_token: usdc,
|
||||
token_in: weth,
|
||||
token_out: usdc,
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
split: 0f64,
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
pub mod approvals;
|
||||
mod constants;
|
||||
pub mod encoder_builder;
|
||||
pub mod encoder_builders;
|
||||
mod group_swaps;
|
||||
pub mod strategy_encoder;
|
||||
mod swap_encoder;
|
||||
pub mod tycho_encoder;
|
||||
pub mod tycho_encoders;
|
||||
pub mod utils;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod group_swaps;
|
||||
pub mod strategy_encoders;
|
||||
mod strategy_validators;
|
||||
|
||||
mod transfer_optimizations;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,92 +7,10 @@ use crate::encoding::{
|
||||
models::{NativeAction, Solution, Swap},
|
||||
};
|
||||
|
||||
/// Validates whether a sequence of split swaps represents a valid solution.
|
||||
#[derive(Clone)]
|
||||
pub struct SplitSwapValidator;
|
||||
|
||||
impl SplitSwapValidator {
|
||||
/// Raises an error if the split percentages are invalid.
|
||||
///
|
||||
/// Split percentages are considered valid if all the following conditions are met:
|
||||
/// * Each split amount is < 1 (100%)
|
||||
/// * There is exactly one 0% split for each token, and it's the last swap specified, signifying
|
||||
/// to the router to send the remainder of the token to the designated protocol
|
||||
/// * The sum of all non-remainder splits for each token is < 1 (100%)
|
||||
/// * There are no negative split amounts
|
||||
pub fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> {
|
||||
let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new();
|
||||
for swap in swaps {
|
||||
if swap.split >= 1.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Split percentage must be less than 1 (100%), got {}",
|
||||
swap.split
|
||||
)));
|
||||
}
|
||||
swaps_by_token
|
||||
.entry(swap.token_in.clone())
|
||||
.or_default()
|
||||
.push(swap);
|
||||
}
|
||||
|
||||
for (token, token_swaps) in swaps_by_token {
|
||||
// Single swaps don't need remainder handling
|
||||
if token_swaps.len() == 1 {
|
||||
if token_swaps[0].split != 0.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Single swap must have 0% split for token {:?}",
|
||||
token
|
||||
)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut found_zero_split = false;
|
||||
let mut total_percentage = 0.0;
|
||||
for (i, swap) in token_swaps.iter().enumerate() {
|
||||
match (swap.split == 0.0, i == token_swaps.len() - 1) {
|
||||
(true, false) => {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"The 0% split for token {:?} must be the last swap",
|
||||
token
|
||||
)))
|
||||
}
|
||||
(true, true) => found_zero_split = true,
|
||||
(false, _) => {
|
||||
if swap.split < 0.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"All splits must be >= 0% for token {:?}",
|
||||
token
|
||||
)));
|
||||
}
|
||||
total_percentage += swap.split;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_zero_split {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Token {:?} must have exactly one 0% split for remainder handling",
|
||||
token
|
||||
)));
|
||||
}
|
||||
|
||||
// Total must be <100% to leave room for remainder
|
||||
if total_percentage >= 1.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Total of non-remainder splits for token {:?} must be <100%, got {}%",
|
||||
token,
|
||||
total_percentage * 100.0
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait SwapValidator {
|
||||
/// Raises an error if the solution does not have checked amount set or slippage with checked
|
||||
/// amount set.
|
||||
pub fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> {
|
||||
fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> {
|
||||
if solution.checked_amount.is_none() &&
|
||||
(solution.slippage.is_none() || solution.expected_amount.is_none())
|
||||
{
|
||||
@@ -113,7 +31,7 @@ impl SplitSwapValidator {
|
||||
/// If the given token is the native token and the native action is WRAP, it will be converted
|
||||
/// to the wrapped token before validating the swap path. The same principle applies for the
|
||||
/// checked token and the UNWRAP action.
|
||||
pub fn validate_swap_path(
|
||||
fn validate_swap_path(
|
||||
&self,
|
||||
swaps: &[Swap],
|
||||
given_token: &Bytes,
|
||||
@@ -197,6 +115,94 @@ impl SplitSwapValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates whether a sequence of split swaps represents a valid solution.
|
||||
#[derive(Clone)]
|
||||
pub struct SplitSwapValidator;
|
||||
|
||||
impl SwapValidator for SplitSwapValidator {}
|
||||
|
||||
impl SplitSwapValidator {
|
||||
/// Raises an error if the split percentages are invalid.
|
||||
///
|
||||
/// Split percentages are considered valid if all the following conditions are met:
|
||||
/// * Each split amount is < 1 (100%)
|
||||
/// * There is exactly one 0% split for each token, and it's the last swap specified, signifying
|
||||
/// to the router to send the remainder of the token to the designated protocol
|
||||
/// * The sum of all non-remainder splits for each token is < 1 (100%)
|
||||
/// * There are no negative split amounts
|
||||
pub fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> {
|
||||
let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new();
|
||||
for swap in swaps {
|
||||
if swap.split >= 1.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Split percentage must be less than 1 (100%), got {}",
|
||||
swap.split
|
||||
)));
|
||||
}
|
||||
swaps_by_token
|
||||
.entry(swap.token_in.clone())
|
||||
.or_default()
|
||||
.push(swap);
|
||||
}
|
||||
|
||||
for (token, token_swaps) in swaps_by_token {
|
||||
// Single swaps don't need remainder handling
|
||||
if token_swaps.len() == 1 {
|
||||
if token_swaps[0].split != 0.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Single swap must have 0% split for token {token}",
|
||||
)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut found_zero_split = false;
|
||||
let mut total_percentage = 0.0;
|
||||
for (i, swap) in token_swaps.iter().enumerate() {
|
||||
match (swap.split == 0.0, i == token_swaps.len() - 1) {
|
||||
(true, false) => {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"The 0% split for token {token} must be the last swap",
|
||||
)))
|
||||
}
|
||||
(true, true) => found_zero_split = true,
|
||||
(false, _) => {
|
||||
if swap.split < 0.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"All splits must be >= 0% for token {token}"
|
||||
)));
|
||||
}
|
||||
total_percentage += swap.split;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_zero_split {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Token {token} must have exactly one 0% split for remainder handling"
|
||||
)));
|
||||
}
|
||||
|
||||
// Total must be <100% to leave room for remainder
|
||||
if total_percentage >= 1.0 {
|
||||
return Err(EncodingError::InvalidInput(format!(
|
||||
"Total of non-remainder splits for token {:?} must be <100%, got {}%",
|
||||
token,
|
||||
total_percentage * 100.0
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates whether a sequence of sequential swaps represents a valid solution.
|
||||
#[derive(Clone)]
|
||||
pub struct SequentialSwapValidator;
|
||||
|
||||
impl SwapValidator for SequentialSwapValidator {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
352
src/encoding/evm/strategy_encoder/transfer_optimizations.rs
Normal file
352
src/encoding/evm/strategy_encoder/transfer_optimizations.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use tycho_common::Bytes;
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::{
|
||||
constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
|
||||
group_swaps::SwapGroup,
|
||||
},
|
||||
models::TransferType,
|
||||
};
|
||||
|
||||
/// A struct that defines how the tokens will be transferred into the given pool given the solution.
|
||||
#[derive(Clone)]
|
||||
pub struct TransferOptimization {
|
||||
native_token: Bytes,
|
||||
wrapped_token: Bytes,
|
||||
permit2: bool,
|
||||
token_in_already_in_router: bool,
|
||||
router_address: Bytes,
|
||||
}
|
||||
|
||||
impl TransferOptimization {
|
||||
pub fn new(
|
||||
native_token: Bytes,
|
||||
wrapped_token: Bytes,
|
||||
permit2: bool,
|
||||
token_in_already_in_router: bool,
|
||||
router_address: Bytes,
|
||||
) -> Self {
|
||||
TransferOptimization {
|
||||
native_token,
|
||||
wrapped_token,
|
||||
permit2,
|
||||
token_in_already_in_router,
|
||||
router_address,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the transfer method that should be used for the given swap and solution.
|
||||
pub fn get_transfer_type(
|
||||
&self,
|
||||
swap: SwapGroup,
|
||||
given_token: Bytes,
|
||||
wrap: bool,
|
||||
in_between_swap_optimization: bool,
|
||||
) -> TransferType {
|
||||
let in_transfer_required: bool =
|
||||
IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.protocol_system.as_str());
|
||||
|
||||
let is_first_swap = swap.token_in == given_token;
|
||||
|
||||
if swap.token_in == self.native_token {
|
||||
// Funds are already in router. All protocols currently take care of native transfers.
|
||||
TransferType::None
|
||||
} else if (swap.token_in == self.wrapped_token) && wrap {
|
||||
// Wrapping already happened in the router so we can just use a normal transfer.
|
||||
TransferType::TransferToProtocol
|
||||
} else if is_first_swap {
|
||||
if in_transfer_required {
|
||||
if self.token_in_already_in_router {
|
||||
// Transfer from router to pool.
|
||||
TransferType::TransferToProtocol
|
||||
} else if self.permit2 {
|
||||
// Transfer from swapper to pool using permit2.
|
||||
TransferType::TransferPermit2ToProtocol
|
||||
} else {
|
||||
// Transfer from swapper to pool.
|
||||
TransferType::TransferFromToProtocol
|
||||
}
|
||||
// in transfer is not necessary for these protocols. Only make a transfer if the
|
||||
// tokens are not already in the router
|
||||
} else if !self.token_in_already_in_router {
|
||||
if self.permit2 {
|
||||
// Transfer from swapper to router using permit2.
|
||||
TransferType::TransferPermit2ToRouter
|
||||
} else {
|
||||
// Transfer from swapper to router.
|
||||
TransferType::TransferFromToRouter
|
||||
}
|
||||
} else {
|
||||
TransferType::None
|
||||
}
|
||||
// all other swaps
|
||||
} else if !in_transfer_required || in_between_swap_optimization {
|
||||
// funds should already be in the router or in the next pool
|
||||
TransferType::None
|
||||
} else {
|
||||
TransferType::TransferToProtocol
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the optimized receiver of the swap. This is used to chain swaps together and avoid
|
||||
// unnecessary token transfers.
|
||||
// Returns the receiver address and a boolean indicating whether the receiver is optimized (this
|
||||
// is necessary for the next swap transfer type decision).
|
||||
pub fn get_receiver(
|
||||
&self,
|
||||
solution_receiver: Bytes,
|
||||
next_swap: Option<&SwapGroup>,
|
||||
) -> Result<(Bytes, bool), EncodingError> {
|
||||
if let Some(next) = next_swap {
|
||||
// if the protocol of the next swap supports transfer in optimization
|
||||
if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
|
||||
// if the protocol does not allow for chained swaps, we can't optimize the
|
||||
// receiver of this swap nor the transfer in of the next swap
|
||||
if CALLBACK_CONSTRAINED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
|
||||
Ok((self.router_address.clone(), false))
|
||||
} else {
|
||||
Ok((
|
||||
Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| {
|
||||
EncodingError::FatalError("Invalid component id".to_string())
|
||||
})?,
|
||||
true,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
// the protocol of the next swap does not support transfer in optimization
|
||||
Ok((self.router_address.clone(), false))
|
||||
}
|
||||
} else {
|
||||
// last swap - there is no next swap
|
||||
Ok((solution_receiver, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloy_primitives::hex;
|
||||
use rstest::rstest;
|
||||
use tycho_common::models::protocol::ProtocolComponent;
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::Swap;
|
||||
|
||||
fn weth() -> Bytes {
|
||||
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
|
||||
}
|
||||
|
||||
fn eth() -> Bytes {
|
||||
Bytes::from(hex!("0000000000000000000000000000000000000000").to_vec())
|
||||
}
|
||||
|
||||
fn dai() -> Bytes {
|
||||
Bytes::from(hex!("6b175474e89094c44da98b954eedeac495271d0f").to_vec())
|
||||
}
|
||||
|
||||
fn usdc() -> Bytes {
|
||||
Bytes::from(hex!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").to_vec())
|
||||
}
|
||||
|
||||
fn router_address() -> Bytes {
|
||||
Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_swap_transfer_from_permit2() {
|
||||
// The swap token is the same as the given token, which is not the native token
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), true, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::TransferPermit2ToProtocol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_swap_transfer_from() {
|
||||
// The swap token is the same as the given token, which is not the native token
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::TransferFromToProtocol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_swap_native() {
|
||||
// The swap token is the same as the given token, and it's the native token.
|
||||
// No transfer action is needed.
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: eth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_swap_wrapped() {
|
||||
// The swap token is NOT the same as the given token, but we are wrapping.
|
||||
// Since the swap's token in is the wrapped token - this is the first swap.
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false);
|
||||
assert_eq!(transfer_method, TransferType::TransferToProtocol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_first_swap() {
|
||||
// The swap token is NOT the same as the given token, and we are NOT wrapping.
|
||||
// Thus, this is not the first swap.
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::TransferToProtocol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_first_swap_funds_in_router() {
|
||||
// Not the first swap and the protocol requires the funds to be in the router (which they
|
||||
// already are, so the transfer type is None)
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "vm:curve".to_string(),
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_first_swap_in_between_swap_optimization() {
|
||||
// Not the first swap and the in between swaps are optimized. The funds should already be in
|
||||
// the next pool or in the router
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true);
|
||||
assert_eq!(transfer_method, TransferType::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_swap_tokens_already_in_router_optimization() {
|
||||
// It is the first swap, tokens are already in the router and the protocol requires the
|
||||
// transfer in
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::TransferToProtocol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_swap_tokens_already_in_router_no_transfer_needed_optimization() {
|
||||
// It is the first swap, tokens are already in the router and the protocol does not require
|
||||
// the transfer in
|
||||
let swap = SwapGroup {
|
||||
protocol_system: "vm:curve".to_string(),
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![],
|
||||
};
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address());
|
||||
let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false);
|
||||
assert_eq!(transfer_method, TransferType::None);
|
||||
}
|
||||
|
||||
fn receiver() -> Bytes {
|
||||
Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
|
||||
}
|
||||
|
||||
fn component_id() -> Bytes {
|
||||
Bytes::from("0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11")
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
// there is no next swap -> receiver is the solution receiver
|
||||
#[case(None, receiver(), false)]
|
||||
// protocol of next swap supports transfer in optimization
|
||||
#[case(Some("uniswap_v2"), component_id(), true)]
|
||||
// protocol of next swap supports transfer in optimization but is callback constrained
|
||||
#[case(Some("uniswap_v3"), router_address(), false)]
|
||||
// protocol of next swap does not support transfer in optimization
|
||||
#[case(Some("vm:curve"), router_address(), false)]
|
||||
fn test_get_receiver(
|
||||
#[case] protocol: Option<&str>,
|
||||
#[case] expected_receiver: Bytes,
|
||||
#[case] expected_optimization: bool,
|
||||
) {
|
||||
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
|
||||
|
||||
let next_swap = if protocol.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(SwapGroup {
|
||||
protocol_system: protocol.unwrap().to_string(),
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: protocol.unwrap().to_string(),
|
||||
id: component_id().to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
}],
|
||||
})
|
||||
};
|
||||
|
||||
let result = optimization.get_receiver(receiver(), next_swap.as_ref());
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (actual_receiver, optimization_flag) = result.unwrap();
|
||||
assert_eq!(actual_receiver, expected_receiver);
|
||||
assert_eq!(optimization_flag, expected_optimization);
|
||||
}
|
||||
}
|
||||
@@ -20,16 +20,11 @@ pub struct SwapEncoderRegistry {
|
||||
impl SwapEncoderRegistry {
|
||||
/// Populates the registry with the `SwapEncoders` for the given blockchain by parsing the
|
||||
/// executors' addresses in the file at the given path.
|
||||
pub fn new(
|
||||
executors_file_path: Option<String>,
|
||||
blockchain: tycho_common::models::Chain,
|
||||
) -> Result<Self, EncodingError> {
|
||||
let chain = Chain::from(blockchain);
|
||||
pub fn new(executors_file_path: Option<String>, chain: Chain) -> Result<Self, EncodingError> {
|
||||
let config_str = if let Some(ref path) = executors_file_path {
|
||||
fs::read_to_string(path).map_err(|e| {
|
||||
EncodingError::FatalError(format!(
|
||||
"Error reading executors file from {:?}: {}",
|
||||
executors_file_path, e
|
||||
"Error reading executors file from {executors_file_path:?}: {e}",
|
||||
))
|
||||
})?
|
||||
} else {
|
||||
|
||||
@@ -66,6 +66,7 @@ impl SwapEncoder for UniswapV2SwapEncoder {
|
||||
component_id,
|
||||
bytes_to_address(&encoding_context.receiver)?,
|
||||
zero_to_one,
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
);
|
||||
|
||||
Ok(args.abi_encode_packed())
|
||||
@@ -128,6 +129,7 @@ impl SwapEncoder for UniswapV3SwapEncoder {
|
||||
bytes_to_address(&encoding_context.receiver)?,
|
||||
component_id,
|
||||
zero_to_one,
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
);
|
||||
|
||||
Ok(args.abi_encode_packed())
|
||||
@@ -196,10 +198,6 @@ impl SwapEncoder for UniswapV4SwapEncoder {
|
||||
let group_token_out_address = bytes_to_address(&encoding_context.group_token_out)?;
|
||||
|
||||
let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address);
|
||||
let callback_executor =
|
||||
bytes_to_address(&Bytes::from_str(&self.executor_address).map_err(|_| {
|
||||
EncodingError::FatalError("Invalid UniswapV4 executor address".into())
|
||||
})?)?;
|
||||
|
||||
let pool_params =
|
||||
(token_out_address, pool_fee_u24, pool_tick_spacing_u24).abi_encode_packed();
|
||||
@@ -208,7 +206,8 @@ impl SwapEncoder for UniswapV4SwapEncoder {
|
||||
group_token_in_address,
|
||||
group_token_out_address,
|
||||
zero_to_one,
|
||||
callback_executor,
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
bytes_to_address(&encoding_context.receiver)?,
|
||||
pool_params,
|
||||
);
|
||||
|
||||
@@ -283,6 +282,7 @@ impl SwapEncoder for BalancerV2SwapEncoder {
|
||||
component_id,
|
||||
bytes_to_address(&encoding_context.receiver)?,
|
||||
approval_needed,
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
);
|
||||
Ok(args.abi_encode_packed())
|
||||
}
|
||||
@@ -344,6 +344,7 @@ impl SwapEncoder for EkuboSwapEncoder {
|
||||
let mut encoded = vec![];
|
||||
|
||||
if encoding_context.group_token_in == swap.token_in {
|
||||
encoded.extend((encoding_context.transfer_type as u8).to_be_bytes());
|
||||
encoded.extend(bytes_to_address(&encoding_context.receiver)?);
|
||||
encoded.extend(bytes_to_address(&swap.token_in)?);
|
||||
}
|
||||
@@ -377,6 +378,7 @@ pub struct CurveSwapEncoder {
|
||||
meta_registry_address: String,
|
||||
native_token_curve_address: String,
|
||||
native_token_address: Bytes,
|
||||
wrapped_native_token_address: Bytes,
|
||||
}
|
||||
|
||||
impl CurveSwapEncoder {
|
||||
@@ -406,8 +408,7 @@ impl CurveSwapEncoder {
|
||||
// StableSwap factory
|
||||
"0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d" => Ok(U8::from(1)),
|
||||
_ => Err(EncodingError::FatalError(format!(
|
||||
"Unsupported curve factory address: {}",
|
||||
factory_address
|
||||
"Unsupported curve factory address: {factory_address}"
|
||||
))),
|
||||
},
|
||||
}
|
||||
@@ -449,10 +450,33 @@ impl CurveSwapEncoder {
|
||||
let j = U8::from(j_256);
|
||||
Ok((i, j))
|
||||
}
|
||||
Err(err) => Err(EncodingError::RecoverableError(format!(
|
||||
"Curve meta registry call failed with error: {:?}",
|
||||
err
|
||||
))),
|
||||
Err(err) => {
|
||||
// Temporary until we get the coin indexes from the indexer
|
||||
// This is because some curve pools hold ETH but the coin is defined as WETH
|
||||
// Our indexer reports this pool as holding ETH but then here we need to use WETH
|
||||
// This is valid only for some pools, that's why we are doing the trial and error
|
||||
// approach
|
||||
let native_token_curve_address =
|
||||
Address::from_str(&self.native_token_curve_address).map_err(|_| {
|
||||
EncodingError::FatalError(
|
||||
"Invalid Curve native token curve address".to_string(),
|
||||
)
|
||||
})?;
|
||||
if token_in != native_token_curve_address && token_out != native_token_curve_address
|
||||
{
|
||||
Err(EncodingError::RecoverableError(format!(
|
||||
"Curve meta registry call failed with error: {err}"
|
||||
)))
|
||||
} else {
|
||||
let wrapped_token = bytes_to_address(&self.wrapped_native_token_address)?;
|
||||
let (i, j) = if token_in == native_token_curve_address {
|
||||
self.get_coin_indexes(pool_id, wrapped_token, token_out)?
|
||||
} else {
|
||||
self.get_coin_indexes(pool_id, token_in, wrapped_token)?
|
||||
};
|
||||
Ok((i, j))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +506,7 @@ impl SwapEncoder for CurveSwapEncoder {
|
||||
executor_address,
|
||||
meta_registry_address,
|
||||
native_token_address: chain.native_token()?,
|
||||
wrapped_native_token_address: chain.wrapped_token()?,
|
||||
native_token_curve_address,
|
||||
})
|
||||
}
|
||||
@@ -535,7 +560,10 @@ impl SwapEncoder for CurveSwapEncoder {
|
||||
})?)
|
||||
.map_err(|_| EncodingError::FatalError("Invalid curve factory address".to_string()))?;
|
||||
|
||||
let pool_type = self.get_pool_type(&swap.component.id, &factory_address.to_string())?;
|
||||
let pool_address = Address::from_str(&swap.component.id)
|
||||
.map_err(|_| EncodingError::FatalError("Invalid curve pool address".to_string()))?;
|
||||
let pool_type =
|
||||
self.get_pool_type(&pool_address.to_string(), &factory_address.to_string())?;
|
||||
|
||||
let (i, j) = self.get_coin_indexes(component_address, token_in, token_out)?;
|
||||
|
||||
@@ -547,6 +575,8 @@ impl SwapEncoder for CurveSwapEncoder {
|
||||
i.to_be_bytes::<1>(),
|
||||
j.to_be_bytes::<1>(),
|
||||
approval_needed,
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
bytes_to_address(&encoding_context.receiver)?,
|
||||
);
|
||||
|
||||
Ok(args.abi_encode_packed())
|
||||
@@ -614,7 +644,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::TransferType;
|
||||
|
||||
mod uniswap_v2 {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_encode_uniswap_v2() {
|
||||
let usv2_pool = ProtocolComponent {
|
||||
@@ -636,6 +669,7 @@ mod tests {
|
||||
router_address: Some(Bytes::zero(20)),
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
let encoder = UniswapV2SwapEncoder::new(
|
||||
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
|
||||
@@ -658,9 +692,15 @@ mod tests {
|
||||
"0000000000000000000000000000000000000001",
|
||||
// zero for one
|
||||
"00",
|
||||
// transfer type (transfer)
|
||||
"00",
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod uniswap_v3 {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_encode_uniswap_v3() {
|
||||
let fee = BigInt::from(500);
|
||||
@@ -687,6 +727,7 @@ mod tests {
|
||||
router_address: Some(Bytes::zero(20)),
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
let encoder = UniswapV3SwapEncoder::new(
|
||||
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
|
||||
@@ -713,14 +754,23 @@ mod tests {
|
||||
"88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
|
||||
// zero for one
|
||||
"00",
|
||||
// transfer type (transfer)
|
||||
"00",
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod balancer_v2 {
|
||||
use super::*;
|
||||
use crate::encoding::evm::utils::write_calldata_to_file;
|
||||
|
||||
#[test]
|
||||
fn test_encode_balancer_v2() {
|
||||
let balancer_pool = ProtocolComponent {
|
||||
id: String::from("0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014"),
|
||||
id: String::from(
|
||||
"0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014",
|
||||
),
|
||||
protocol_system: String::from("vm:balancer_v2"),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -739,6 +789,7 @@ mod tests {
|
||||
router_address: Some(Bytes::zero(20)),
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::None,
|
||||
};
|
||||
let encoder = BalancerV2SwapEncoder::new(
|
||||
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
|
||||
@@ -766,10 +817,18 @@ mod tests {
|
||||
// receiver
|
||||
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
|
||||
// approval needed
|
||||
"01"
|
||||
"01",
|
||||
// transfer type
|
||||
"05"
|
||||
))
|
||||
);
|
||||
write_calldata_to_file("test_encode_balancer_v2", hex_swap.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
mod uniswap_v4 {
|
||||
use super::*;
|
||||
use crate::encoding::evm::utils::write_calldata_to_file;
|
||||
|
||||
#[test]
|
||||
fn test_encode_uniswap_v4_simple_swap() {
|
||||
@@ -796,15 +855,15 @@ mod tests {
|
||||
split: 0f64,
|
||||
};
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver address was taken from `address(uniswapV4Exposed)` in the
|
||||
// UniswapV4Executor.t.sol
|
||||
receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"),
|
||||
// The receiver is ALICE to match the solidity tests
|
||||
receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"),
|
||||
exact_out: false,
|
||||
// Same as the executor address
|
||||
router_address: Some(Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")),
|
||||
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
let encoder = UniswapV4SwapEncoder::new(
|
||||
String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"),
|
||||
@@ -816,7 +875,6 @@ mod tests {
|
||||
.encode_swap(swap, encoding_context)
|
||||
.unwrap();
|
||||
let hex_swap = encode(&encoded_swap);
|
||||
println!("{}", hex_swap);
|
||||
|
||||
assert_eq!(
|
||||
hex_swap,
|
||||
@@ -827,8 +885,10 @@ mod tests {
|
||||
"dac17f958d2ee523a2206206994597c13d831ec7",
|
||||
// zero for one
|
||||
"01",
|
||||
// executor address
|
||||
"f62849f9a0b5bf2913b396098f7c7019b51a820a",
|
||||
// transfer type
|
||||
"00",
|
||||
// receiver
|
||||
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
|
||||
// pool params:
|
||||
// - intermediary token
|
||||
"dac17f958d2ee523a2206206994597c13d831ec7",
|
||||
@@ -838,6 +898,7 @@ mod tests {
|
||||
"000001"
|
||||
))
|
||||
);
|
||||
write_calldata_to_file("test_encode_uniswap_v4_simple_swap", hex_swap.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -873,6 +934,7 @@ mod tests {
|
||||
group_token_in: group_token_in.clone(),
|
||||
// Token out is the same as the group token out
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
|
||||
let encoder = UniswapV4SwapEncoder::new(
|
||||
@@ -906,15 +968,16 @@ mod tests {
|
||||
let usdt_address = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7");
|
||||
let wbtc_address = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
|
||||
let router_address = Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f");
|
||||
let receiver_address = router_address.clone();
|
||||
|
||||
// The context is the same for both swaps, since the group token in and out are the same
|
||||
let context = EncodingContext {
|
||||
receiver: receiver_address.clone(),
|
||||
// The receiver is ALICE to match the solidity tests
|
||||
receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"),
|
||||
exact_out: false,
|
||||
router_address: Some(router_address.clone()),
|
||||
group_token_in: usde_address.clone(),
|
||||
group_token_out: wbtc_address.clone(),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
|
||||
// Setup - First sequence: USDE -> USDT
|
||||
@@ -992,8 +1055,10 @@ mod tests {
|
||||
"2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
||||
// zero for one
|
||||
"01",
|
||||
// executor address
|
||||
"f62849f9a0b5bf2913b396098f7c7019b51a820a",
|
||||
// transfer type
|
||||
"00",
|
||||
// receiver
|
||||
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
|
||||
// pool params:
|
||||
// - intermediary token USDT
|
||||
"dac17f958d2ee523a2206206994597c13d831ec7",
|
||||
@@ -1009,10 +1074,12 @@ mod tests {
|
||||
"00003c"
|
||||
))
|
||||
);
|
||||
write_calldata_to_file("test_encode_uniswap_v4_sequential_swap", combined_hex.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
mod ekubo {
|
||||
use super::*;
|
||||
use crate::encoding::evm::utils::write_calldata_to_file;
|
||||
|
||||
const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address
|
||||
|
||||
@@ -1045,6 +1112,7 @@ mod tests {
|
||||
group_token_out: token_out.clone(),
|
||||
exact_out: false,
|
||||
router_address: Some(Bytes::default()),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
|
||||
let encoder =
|
||||
@@ -1059,8 +1127,11 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
hex_swap,
|
||||
RECEIVER.to_string() +
|
||||
concat!(
|
||||
// transfer type
|
||||
"00",
|
||||
// receiver
|
||||
"ca4f73fe97d0b987a0d12b39bbd562c779bab6f6",
|
||||
// group token in
|
||||
"0000000000000000000000000000000000000000",
|
||||
// token out 1st swap
|
||||
@@ -1087,6 +1158,7 @@ mod tests {
|
||||
group_token_out: group_token_out.clone(),
|
||||
exact_out: false,
|
||||
router_address: Some(Bytes::default()),
|
||||
transfer_type: TransferType::TransferToProtocol,
|
||||
};
|
||||
|
||||
let first_swap = Swap {
|
||||
@@ -1132,12 +1204,14 @@ mod tests {
|
||||
let combined_hex =
|
||||
format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap));
|
||||
|
||||
println!("{}", combined_hex);
|
||||
|
||||
assert_eq!(
|
||||
combined_hex,
|
||||
RECEIVER.to_string() +
|
||||
// transfer type
|
||||
concat!(
|
||||
// transfer type
|
||||
"00",
|
||||
// receiver
|
||||
"ca4f73fe97d0b987a0d12b39bbd562c779bab6f6",
|
||||
// group token in
|
||||
"0000000000000000000000000000000000000000",
|
||||
// token out 1st swap
|
||||
@@ -1150,6 +1224,7 @@ mod tests {
|
||||
"00000000000000000000000000000000000000000001a36e2eb1c43200000032",
|
||||
),
|
||||
);
|
||||
write_calldata_to_file("test_ekubo_encode_swap_multi", combined_hex.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1207,6 +1282,22 @@ mod tests {
|
||||
2,
|
||||
0
|
||||
)]
|
||||
// Pool that holds ETH but coin is WETH
|
||||
#[case(
|
||||
"0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B",
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
2,
|
||||
0
|
||||
)]
|
||||
// Pool that holds ETH but coin is WETH
|
||||
#[case(
|
||||
"0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B",
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
0,
|
||||
2
|
||||
)]
|
||||
fn test_curve_get_coin_indexes(
|
||||
#[case] pool: &str,
|
||||
#[case] token_in: &str,
|
||||
@@ -1263,6 +1354,7 @@ mod tests {
|
||||
router_address: None,
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::None,
|
||||
};
|
||||
let encoder = CurveSwapEncoder::new(
|
||||
String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"),
|
||||
@@ -1292,6 +1384,10 @@ mod tests {
|
||||
"01",
|
||||
// approval needed
|
||||
"01",
|
||||
// transfer type
|
||||
"05",
|
||||
// receiver,
|
||||
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"
|
||||
))
|
||||
);
|
||||
}
|
||||
@@ -1328,6 +1424,7 @@ mod tests {
|
||||
router_address: None,
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::None,
|
||||
};
|
||||
let encoder = CurveSwapEncoder::new(
|
||||
String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"),
|
||||
@@ -1357,6 +1454,10 @@ mod tests {
|
||||
"00",
|
||||
// approval needed
|
||||
"01",
|
||||
// transfer type
|
||||
"05",
|
||||
// receiver
|
||||
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"
|
||||
))
|
||||
);
|
||||
}
|
||||
@@ -1394,6 +1495,7 @@ mod tests {
|
||||
router_address: None,
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::None,
|
||||
};
|
||||
let encoder = CurveSwapEncoder::new(
|
||||
String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"),
|
||||
@@ -1432,6 +1534,10 @@ mod tests {
|
||||
"01",
|
||||
// approval needed
|
||||
"01",
|
||||
// transfer type
|
||||
"05",
|
||||
// receiver
|
||||
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,720 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use num_bigint::BigUint;
|
||||
use tycho_common::Bytes;
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
models::{Chain, NativeAction, Solution, Transaction},
|
||||
strategy_encoder::StrategyEncoder,
|
||||
tycho_encoder::TychoEncoder,
|
||||
};
|
||||
|
||||
/// Represents an encoder for a swap using any strategy supported by the strategy registry.
|
||||
///
|
||||
/// # Fields
|
||||
/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution
|
||||
/// * `native_address`: Address of the chain's native token
|
||||
/// * `wrapped_address`: Address of the chain's wrapped native token
|
||||
pub struct EVMTychoEncoder {
|
||||
strategy_encoder: Box<dyn StrategyEncoder>,
|
||||
native_address: Bytes,
|
||||
wrapped_address: Bytes,
|
||||
}
|
||||
|
||||
impl Clone for EVMTychoEncoder {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
strategy_encoder: self.strategy_encoder.clone_box(),
|
||||
native_address: self.native_address.clone(),
|
||||
wrapped_address: self.wrapped_address.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EVMTychoEncoder {
|
||||
pub fn new(
|
||||
chain: tycho_common::models::Chain,
|
||||
strategy_encoder: Box<dyn StrategyEncoder>,
|
||||
) -> Result<Self, EncodingError> {
|
||||
let chain: Chain = Chain::from(chain);
|
||||
let native_address = chain.native_token()?;
|
||||
let wrapped_address = chain.wrapped_token()?;
|
||||
Ok(EVMTychoEncoder { strategy_encoder, native_address, wrapped_address })
|
||||
}
|
||||
}
|
||||
|
||||
impl EVMTychoEncoder {
|
||||
/// Raises an `EncodingError` if the solution is not considered valid.
|
||||
///
|
||||
/// A solution is considered valid if all the following conditions are met:
|
||||
/// * The solution is not exact out.
|
||||
/// * The solution has at least one swap.
|
||||
/// * If the solution is wrapping, the given token is the chain's native token and the first
|
||||
/// swap's input is the chain's wrapped token.
|
||||
/// * If the solution is unwrapping, the checked token is the chain's native token and the last
|
||||
/// swap's output is the chain's wrapped token.
|
||||
/// * The token cannot appear more than once in the solution unless it is the first and last
|
||||
/// token (i.e. a true cyclical swap).
|
||||
fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> {
|
||||
if solution.exact_out {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Currently only exact input solutions are supported".to_string(),
|
||||
));
|
||||
}
|
||||
if solution.swaps.is_empty() {
|
||||
return Err(EncodingError::FatalError("No swaps found in solution".to_string()));
|
||||
}
|
||||
if let Some(native_action) = solution.clone().native_action {
|
||||
if native_action == NativeAction::Wrap {
|
||||
if solution.given_token != self.native_address {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Native token must be the input token in order to wrap".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(first_swap) = solution.swaps.first() {
|
||||
if first_swap.token_in != self.wrapped_address {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Wrapped token must be the first swap's input in order to wrap"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if native_action == NativeAction::Unwrap {
|
||||
if solution.checked_token != self.native_address {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Native token must be the output token in order to unwrap".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(last_swap) = solution.swaps.last() {
|
||||
if last_swap.token_out != self.wrapped_address {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Wrapped token must be the last swap's output in order to unwrap"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut solution_tokens = vec![];
|
||||
let mut split_tokens_already_considered = HashSet::new();
|
||||
for (i, swap) in solution.swaps.iter().enumerate() {
|
||||
// so we don't count the split tokens more than once
|
||||
if swap.split != 0.0 {
|
||||
if !split_tokens_already_considered.contains(&swap.token_in) {
|
||||
solution_tokens.push(swap.token_in.clone());
|
||||
split_tokens_already_considered.insert(swap.token_in.clone());
|
||||
}
|
||||
} else {
|
||||
// it might be the last swap of the split or a regular swap
|
||||
if !split_tokens_already_considered.contains(&swap.token_in) {
|
||||
solution_tokens.push(swap.token_in.clone());
|
||||
}
|
||||
}
|
||||
if i == solution.swaps.len() - 1 {
|
||||
solution_tokens.push(swap.token_out.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if solution_tokens.len() !=
|
||||
solution_tokens
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<Bytes>>()
|
||||
.len()
|
||||
{
|
||||
if let Some(last_swap) = solution.swaps.last() {
|
||||
if solution.swaps[0].token_in != last_swap.token_out {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(),
|
||||
));
|
||||
} else {
|
||||
// it is a valid cyclical swap
|
||||
// we don't support any wrapping or unwrapping in this case
|
||||
if let Some(_native_action) = solution.clone().native_action {
|
||||
return Err(EncodingError::FatalError(
|
||||
"Wrapping/Unwrapping is not available in cyclical swaps".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TychoEncoder for EVMTychoEncoder {
|
||||
fn encode_router_calldata(
|
||||
&self,
|
||||
solutions: Vec<Solution>,
|
||||
) -> Result<Vec<Transaction>, EncodingError> {
|
||||
let mut transactions: Vec<Transaction> = Vec::new();
|
||||
for solution in solutions.iter() {
|
||||
self.validate_solution(solution)?;
|
||||
|
||||
let (contract_interaction, target_address) = self
|
||||
.strategy_encoder
|
||||
.encode_strategy(solution.clone())?;
|
||||
|
||||
let value = if solution.given_token == self.native_address {
|
||||
solution.given_amount.clone()
|
||||
} else {
|
||||
BigUint::ZERO
|
||||
};
|
||||
|
||||
transactions.push(Transaction {
|
||||
value,
|
||||
data: contract_interaction,
|
||||
to: target_address,
|
||||
});
|
||||
}
|
||||
Ok(transactions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::{
|
||||
models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder,
|
||||
};
|
||||
|
||||
fn dai() -> Bytes {
|
||||
Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
|
||||
}
|
||||
|
||||
fn eth() -> Bytes {
|
||||
Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap()
|
||||
}
|
||||
|
||||
fn weth() -> Bytes {
|
||||
Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap()
|
||||
}
|
||||
|
||||
fn usdc() -> Bytes {
|
||||
Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap()
|
||||
}
|
||||
|
||||
fn wbtc() -> Bytes {
|
||||
Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockStrategy;
|
||||
|
||||
impl StrategyEncoder for MockStrategy {
|
||||
fn encode_strategy(&self, _solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError> {
|
||||
Ok((
|
||||
Bytes::from_str("0x1234")
|
||||
.unwrap()
|
||||
.to_vec(),
|
||||
Bytes::from_str("0xabcd").unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box<dyn SwapEncoder>> {
|
||||
None
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn StrategyEncoder> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mocked_tycho_encoder() -> EVMTychoEncoder {
|
||||
let strategy_encoder = Box::new(MockStrategy {});
|
||||
EVMTychoEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_router_calldata() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let eth_amount_in = BigUint::from(1000u32);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_amount: eth_amount_in.clone(),
|
||||
given_token: eth(),
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Wrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let transactions = encoder.encode_router_calldata(vec![solution]);
|
||||
|
||||
assert!(transactions.is_ok());
|
||||
let transactions = transactions.unwrap();
|
||||
assert_eq!(transactions.len(), 1);
|
||||
assert_eq!(transactions[0].value, eth_amount_in);
|
||||
assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap());
|
||||
assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_fails_for_exact_out() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let solution = Solution {
|
||||
exact_out: true, // This should cause an error
|
||||
..Default::default()
|
||||
};
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Currently only exact input solutions are supported".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_passes_for_wrap() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: eth(),
|
||||
checked_token: dai(),
|
||||
checked_amount: None,
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Wrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_fails_for_wrap_wrong_input() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: weth(),
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Wrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Native token must be the input token in order to wrap".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_fails_for_wrap_wrong_first_swap() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: eth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: eth(),
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Wrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Wrapped token must be the first swap's input in order to wrap".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_fails_no_swaps() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: eth(),
|
||||
swaps: vec![],
|
||||
native_action: Some(NativeAction::Wrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError("No swaps found in solution".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_passes_for_unwrap() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
checked_token: eth(),
|
||||
checked_amount: None,
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Unwrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_fails_for_unwrap_wrong_output() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: dai(),
|
||||
checked_token: weth(),
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Unwrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Native token must be the output token in order to unwrap".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_fails_for_unwrap_wrong_last_swap() {
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: eth(),
|
||||
split: 0f64,
|
||||
};
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
checked_token: eth(),
|
||||
swaps: vec![swap],
|
||||
native_action: Some(NativeAction::Unwrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Wrapped token must be the last swap's output in order to unwrap".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_cyclical_swap() {
|
||||
// This validation passes because the cyclical swap is the first and last token
|
||||
// 50% -> WETH
|
||||
// DAI - -> DAI
|
||||
// 50% -> WETH
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0.5f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
},
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: dai(),
|
||||
checked_token: dai(),
|
||||
swaps,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_cyclical_swap_fail() {
|
||||
// This test should fail because the cyclical swap is not the first and last token
|
||||
// DAI -> WETH -> USDC -> DAI -> WBTC
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: usdc(),
|
||||
split: 0f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: wbtc(),
|
||||
split: 0f64,
|
||||
},
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: dai(),
|
||||
checked_token: wbtc(),
|
||||
swaps,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Cyclical swaps are only allowed if they are the first and last token of a solution".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_validate_cyclical_swap_split_output() {
|
||||
// This validation passes because it is a valid cyclical swap
|
||||
// -> WETH
|
||||
// WETH -> DAI
|
||||
// -> WETH
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0.5f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
},
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: weth(),
|
||||
checked_token: weth(),
|
||||
swaps,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_cyclical_swap_native_action_fail() {
|
||||
// This validation fails because there is a native action with a valid cyclical swap
|
||||
// ETH -> WETH -> DAI -> WETH
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_mocked_tycho_encoder();
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
},
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: eth(),
|
||||
checked_token: weth(),
|
||||
swaps,
|
||||
native_action: Some(NativeAction::Wrap),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = encoder.validate_solution(&solution);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
EncodingError::FatalError(
|
||||
"Wrapping/Unwrapping is not available in cyclical swaps"
|
||||
.to_string()
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
1075
src/encoding/evm/tycho_encoders.rs
Normal file
1075
src/encoding/evm/tycho_encoders.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,19 @@
|
||||
use std::{cmp::max, env, sync::Arc};
|
||||
use std::{
|
||||
cmp::max,
|
||||
env,
|
||||
fs::OpenOptions,
|
||||
io::{BufRead, BufReader, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
providers::{ProviderBuilder, RootProvider},
|
||||
transports::BoxTransport,
|
||||
};
|
||||
use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8};
|
||||
use alloy_sol_types::SolValue;
|
||||
use num_bigint::BigUint;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::runtime::{Handle, Runtime};
|
||||
use tycho_common::Bytes;
|
||||
|
||||
@@ -22,7 +30,7 @@ pub fn bytes_to_address(address: &Bytes) -> Result<Address, EncodingError> {
|
||||
if address.len() == 20 {
|
||||
Ok(Address::from_slice(address))
|
||||
} else {
|
||||
Err(EncodingError::InvalidInput(format!("Invalid address: {:?}", address)))
|
||||
Err(EncodingError::InvalidInput(format!("Invalid address: {address}",)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +102,7 @@ pub fn get_token_position(tokens: Vec<Bytes>, token: Bytes) -> Result<U8, Encodi
|
||||
.iter()
|
||||
.position(|t| *t == token)
|
||||
.ok_or_else(|| {
|
||||
EncodingError::InvalidInput(format!("Token {:?} not found in tokens array", token))
|
||||
EncodingError::InvalidInput(format!("Token {token} not found in tokens array"))
|
||||
})?,
|
||||
);
|
||||
Ok(position)
|
||||
@@ -120,9 +128,7 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>
|
||||
.component
|
||||
.static_attributes
|
||||
.get(attribute_name)
|
||||
.ok_or_else(|| {
|
||||
EncodingError::FatalError(format!("Attribute {} not found", attribute_name))
|
||||
})?
|
||||
.ok_or_else(|| EncodingError::FatalError(format!("Attribute {attribute_name} not found")))?
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
@@ -150,6 +156,66 @@ pub async fn get_client() -> Result<Arc<RootProvider<BoxTransport>>, EncodingErr
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
|
||||
/// Uses prefix-length encoding to efficient encode action data.
|
||||
///
|
||||
/// Prefix-length encoding is a data encoding method where the beginning of a data segment
|
||||
/// (the "prefix") contains information about the length of the following data.
|
||||
pub fn ple_encode(action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut encoded_action_data: Vec<u8> = Vec::new();
|
||||
|
||||
for action_data in action_data_array {
|
||||
let args = (encoded_action_data, action_data.len() as u16, action_data);
|
||||
encoded_action_data = args.abi_encode_packed();
|
||||
}
|
||||
|
||||
encoded_action_data
|
||||
}
|
||||
|
||||
static CALLDATA_WRITE_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
// Function used in tests to write calldata to a file that then is used by the corresponding
|
||||
// solidity tests.
|
||||
pub fn write_calldata_to_file(test_identifier: &str, hex_calldata: &str) {
|
||||
let _lock = CALLDATA_WRITE_MUTEX
|
||||
.lock()
|
||||
.expect("Couldn't acquire lock");
|
||||
|
||||
let file_path = "foundry/test/assets/calldata.txt";
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(file_path)
|
||||
.expect("Failed to open calldata file for reading");
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let mut found = false;
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
let mut parts = line.splitn(2, ':'); // split at the :
|
||||
let key = parts.next().unwrap_or("");
|
||||
if key == test_identifier {
|
||||
lines.push(format!("{test_identifier}:{hex_calldata}"));
|
||||
found = true;
|
||||
} else {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// If the test identifier wasn't found, append a new line
|
||||
if !found {
|
||||
lines.push(format!("{test_identifier}:{hex_calldata}"));
|
||||
}
|
||||
|
||||
// Write the updated contents back to the file
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)
|
||||
.expect("Failed to open calldata file for writing");
|
||||
|
||||
for line in lines {
|
||||
writeln!(file, "{line}").expect("Failed to write calldata");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_bigint::BigUint;
|
||||
|
||||
@@ -2,7 +2,7 @@ use hex;
|
||||
use num_bigint::BigUint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tycho_common::{
|
||||
models::{protocol::ProtocolComponent, Chain as TychoCoreChain},
|
||||
models::{protocol::ProtocolComponent, Chain as TychoCommonChain},
|
||||
Bytes,
|
||||
};
|
||||
|
||||
@@ -96,6 +96,27 @@ pub struct Transaction {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Represents the type of transfer to be performed into the pool.
|
||||
///
|
||||
/// # Fields
|
||||
///
|
||||
/// * `TransferToProtocol`: Transfer the token from the router into the protocol.
|
||||
/// * `TransferFromToProtocol`: Transfer the token from the sender to the protocol.
|
||||
/// * `TransferPermit2ToProtocol`: Transfer the token from the sender to the protocol using Permit2.
|
||||
/// * `TransferFromToRouter`: Transfer the token from the sender to the router.
|
||||
/// * `TransferPermit2ToRouter`: Transfer the token from the sender to the router using Permit2.
|
||||
/// * `None`: No transfer is needed. Tokens are already in the pool.
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TransferType {
|
||||
TransferToProtocol = 0,
|
||||
TransferFromToProtocol = 1,
|
||||
TransferPermit2ToProtocol = 2,
|
||||
TransferFromToRouter = 3,
|
||||
TransferPermit2ToRouter = 4,
|
||||
None = 5,
|
||||
}
|
||||
|
||||
/// Represents necessary attributes for encoding an order.
|
||||
///
|
||||
/// # Fields
|
||||
@@ -113,6 +134,7 @@ pub struct EncodingContext {
|
||||
pub router_address: Option<Bytes>,
|
||||
pub group_token_in: Bytes,
|
||||
pub group_token_out: Bytes,
|
||||
pub transfer_type: TransferType,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
@@ -121,15 +143,15 @@ pub struct Chain {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<TychoCoreChain> for Chain {
|
||||
fn from(chain: TychoCoreChain) -> Self {
|
||||
impl From<TychoCommonChain> for Chain {
|
||||
fn from(chain: TychoCommonChain) -> Self {
|
||||
match chain {
|
||||
TychoCoreChain::Ethereum => Chain { id: 1, name: chain.to_string() },
|
||||
TychoCoreChain::ZkSync => Chain { id: 324, name: chain.to_string() },
|
||||
TychoCoreChain::Arbitrum => Chain { id: 42161, name: chain.to_string() },
|
||||
TychoCoreChain::Starknet => Chain { id: 0, name: chain.to_string() },
|
||||
TychoCoreChain::Base => Chain { id: 8453, name: chain.to_string() },
|
||||
TychoCoreChain::Unichain => Chain { id: 130, name: chain.to_string() },
|
||||
TychoCommonChain::Ethereum => Chain { id: 1, name: chain.to_string() },
|
||||
TychoCommonChain::ZkSync => Chain { id: 324, name: chain.to_string() },
|
||||
TychoCommonChain::Arbitrum => Chain { id: 42161, name: chain.to_string() },
|
||||
TychoCommonChain::Starknet => Chain { id: 0, name: chain.to_string() },
|
||||
TychoCommonChain::Base => Chain { id: 8453, name: chain.to_string() },
|
||||
TychoCommonChain::Unichain => Chain { id: 130, name: chain.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ pub trait TychoEncoder {
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<Transaction>, EncodingError>` - Vector of executable transactions
|
||||
fn encode_router_calldata(
|
||||
&self,
|
||||
solutions: Vec<Solution>,
|
||||
) -> Result<Vec<Transaction>, EncodingError>;
|
||||
fn encode_calldata(&self, solutions: Vec<Solution>) -> Result<Vec<Transaction>, EncodingError>;
|
||||
|
||||
fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user