diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e854a..eb25ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,119 @@ +## [1.0.1](https://github.com/propeller-heads/tycho-execution/compare/1.0.0...1.0.1) (2025-04-23) + + +### Bug Fixes + +* Changes after rebase ([d39a2ac](https://github.com/propeller-heads/tycho-execution/commit/d39a2accce2d533f8d26223156853b442c02355b)) +* Configurable fee on USV2 executor. ([826aa54](https://github.com/propeller-heads/tycho-execution/commit/826aa54631b71fd209c6cc56c90dc373a4da966d)) +* Tighten max feeBps in USV2 executor ([e07573b](https://github.com/propeller-heads/tycho-execution/commit/e07573bbb8db7ab5c88728dcaea8f537bc2520d4)) + +## [1.0.0](https://github.com/propeller-heads/tycho-execution/compare/0.81.0...1.0.0) (2025-04-22) + + +### Features + +* (WIP) Support selection of transfer into router ([528bd74](https://github.com/propeller-heads/tycho-execution/commit/528bd746f7546ff2cca35c0397af073c8cbdb39b)) +* Add sequential swap methods ([2e3ffa2](https://github.com/propeller-heads/tycho-execution/commit/2e3ffa2c70f5af63a5f3b11556cd87ce535da3dc)) +* Add SequentialSwap integration test with regular approvals ([8836598](https://github.com/propeller-heads/tycho-execution/commit/8836598e5348d9ef48b1777619b89bdb26db842e)) +* Add SingleSwap integration test and fix bug in method signatures ([9c4dfef](https://github.com/propeller-heads/tycho-execution/commit/9c4dfef80de051f24979bc81f4f66f22f93e5f02)) +* Add TokenTransfer class to BalancerV2 ([8e7dd0b](https://github.com/propeller-heads/tycho-execution/commit/8e7dd0b9e7749996f25c82a4be4cd9e4101e6308)) +* Add TokenTransfer class to Curve ([0e0dff7](https://github.com/propeller-heads/tycho-execution/commit/0e0dff7f5a712a1271c030e968d263239f450c6d)) +* Add transfer out for Curve ([005c5a6](https://github.com/propeller-heads/tycho-execution/commit/005c5a6207d449ac59fd117156e487fb228a08c7)) +* Add transfer out for Uniswap V4 ([534e4f0](https://github.com/propeller-heads/tycho-execution/commit/534e4f00db3005e6d4eda363721818b9661d11d6)) +* Allow for token_in_already_in_router ([655c0b2](https://github.com/propeller-heads/tycho-execution/commit/655c0b263590d84d2f86c7b86db1a6be2308107c)) +* allow to pass msg.sender to USV3 callback ([bf4b229](https://github.com/propeller-heads/tycho-execution/commit/bf4b229268bcdd4aeb7e127a01dada3328634da0)) +* allow to pass msg.sender to USV3 callback ([e71a0bb](https://github.com/propeller-heads/tycho-execution/commit/e71a0bb3549fea326996fc378442a353d30d620f)) +* Decode single and sequential swaps in LibSwap ([9be4845](https://github.com/propeller-heads/tycho-execution/commit/9be48456e5b573b74ade5b3cedd4e58a1a2967dc)) +* Delete EVMStrategyEncoder (this is now unnecessary) ([1fabef7](https://github.com/propeller-heads/tycho-execution/commit/1fabef7a972e795e3adbbbcd63a38cb597de8f12)) +* Do not use V4Router for uniswap v4 ([09b5a73](https://github.com/propeller-heads/tycho-execution/commit/09b5a732efe32e36cca4f7bf406bbaf394932284)) +* ExecutorTransferMethods helper contract ([abd9db9](https://github.com/propeller-heads/tycho-execution/commit/abd9db937df58ea76bc45744d7c0fa34226d9357)) +* ExecutorTransferMethods in UniswapV3Executor ([dbbd30e](https://github.com/propeller-heads/tycho-execution/commit/dbbd30e5964fbae6a82ff2a09a82e0d041774362)) +* ExecutorTransferMethods in UniswapV3Executor ([e5426f3](https://github.com/propeller-heads/tycho-execution/commit/e5426f3038325038b63e8c5389b83b205b18ffe4)) +* Fix rollFork usage for Ekubo test ([d7b5e6d](https://github.com/propeller-heads/tycho-execution/commit/d7b5e6dba5e0ae8fbf67dfb102fca51477f0127b)) +* Make USV3 callback work with direct executor call ([51a7910](https://github.com/propeller-heads/tycho-execution/commit/51a791084448842e70c22e457e5318c778cfebb3)) +* No more fee taking ([89f9121](https://github.com/propeller-heads/tycho-execution/commit/89f9121e4ca2d91260bb883a3a62c2caaae3205c)) +* Optimize transfer to first pool ([bf63696](https://github.com/propeller-heads/tycho-execution/commit/bf63696142aa33452690bb7a7088716a9a8dac7b)) +* Proper USV2Executor transfer decoding + tests ([b7ff870](https://github.com/propeller-heads/tycho-execution/commit/b7ff870a7cb318bdd89d6563bdbd2a73a9f67925)) +* Proper USV2Executor transfer decoding + tests ([9dce2c7](https://github.com/propeller-heads/tycho-execution/commit/9dce2c7465eaf6a68ccdd40134bf9c82336f3401)) +* Proper USV3Executor transfer decoding + tests ([1e37320](https://github.com/propeller-heads/tycho-execution/commit/1e373200875097526a558c8b0aa2b93389c138bc)) +* 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 ([1996fd3](https://github.com/propeller-heads/tycho-execution/commit/1996fd39e848aede11fbff7c8f3f914931c6b28a)) +* sequential swap solution validation ([efa5096](https://github.com/propeller-heads/tycho-execution/commit/efa50966613f94abcc27b74e8f08b6322ec75b28)) +* Single swap methods. ([72ccadc](https://github.com/propeller-heads/tycho-execution/commit/72ccadcaaebda9ca3ea16a9eb88dc0e73651b76e)) +* SingleSwapStrategyEncoder ([5efeb8b](https://github.com/propeller-heads/tycho-execution/commit/5efeb8b305d20aefb7876b34d232bbe92f65624f)) +* SingleSwapStrategyEncoder ([03f6961](https://github.com/propeller-heads/tycho-execution/commit/03f6961b13d064352b2dd8ec7ab7d591d1e12ba5)) +* SingleSwapStrategyEncoder ([dbf5d1d](https://github.com/propeller-heads/tycho-execution/commit/dbf5d1ddda1d84325d4562a1660a92d5524ab853)) +* Support in between swaps optimizations ([83d3721](https://github.com/propeller-heads/tycho-execution/commit/83d3721bf19ee6fc6a4fffd58e9e89b515c2473e)) +* Support out transfer straight to the receiver ([d28c254](https://github.com/propeller-heads/tycho-execution/commit/d28c254225e3388adcf6df596c40b15e7d335ddc)) +* Support returning values from the callback ([fb35a53](https://github.com/propeller-heads/tycho-execution/commit/fb35a5305a91c377b9617965b4ed36c06be3bd42)) +* Support using the TransferType in uniswap v4 ([2ecbabe](https://github.com/propeller-heads/tycho-execution/commit/2ecbabeafc36b7d47247e44388c53f3015f832d7)) +* **tycho-router-encoder:** Select strategy depending on the solution ([7f14f1a](https://github.com/propeller-heads/tycho-execution/commit/7f14f1a4ffe2f108fb2e25f030a40e3e344eefce)) +* Use TokenTransfer optimization helper in Ekubo ([d4e8642](https://github.com/propeller-heads/tycho-execution/commit/d4e864272d346c9020f4e21c6953a980f58b6e5b)) + + +### Bug Fixes + +* Add slither ignore for loop call ([15f4ed5](https://github.com/propeller-heads/tycho-execution/commit/15f4ed5d36ba743c1a5f82b2ca903a0272f42f8f)) +* After rebase fixes ([ae30218](https://github.com/propeller-heads/tycho-execution/commit/ae30218842d23ecea0331f6bc086e9be08250911)) +* bad merge ([03ef744](https://github.com/propeller-heads/tycho-execution/commit/03ef744373429fc245d1aa6b4a726f1c0bfed20a)) +* Bring back receiver address zero check ([8e60b6b](https://github.com/propeller-heads/tycho-execution/commit/8e60b6beba77e0dd729b9621c320d7abd5454733)) +* Calldata size for Ekubo pay callback ([fb855d0](https://github.com/propeller-heads/tycho-execution/commit/fb855d00a445904301e2b5a7dd6004ce7b58edbc)) +* Conscious slither silencing ([a645fc7](https://github.com/propeller-heads/tycho-execution/commit/a645fc72ab3082e9d8526428c7738aca7d5a07be)) +* consider wrapping scenario when getting transfer type ([028e860](https://github.com/propeller-heads/tycho-execution/commit/028e8605a173b2407e7a3113337d3937656a2fe6)) +* Fix after merge with main ([3de5a19](https://github.com/propeller-heads/tycho-execution/commit/3de5a192b645ed337addc2b2923109d45339a890)) +* Fix executor address in test and remove duplicated test ([9456dc7](https://github.com/propeller-heads/tycho-execution/commit/9456dc7b0bb3df70c36811366c94a1716123ed57)) +* Fix integration tests with transfer in method support ([0f3a910](https://github.com/propeller-heads/tycho-execution/commit/0f3a9101b9e3f98ae27fa55bcd14d4ce78449982)) +* fix slither CI action ([7431b26](https://github.com/propeller-heads/tycho-execution/commit/7431b266e169e442aacbb8113273151aca5145b8)) +* Fixes after merge with feature branch ([26e6c6c](https://github.com/propeller-heads/tycho-execution/commit/26e6c6c2664105869ae6a418477d8ae37fc6da84)) +* Integration tests after merge ([55242fb](https://github.com/propeller-heads/tycho-execution/commit/55242fb8c4bc506c0cb9f649d00a88469f28f72a)) +* No more EVMStrategyEncoder ([fbbc4c5](https://github.com/propeller-heads/tycho-execution/commit/fbbc4c5277eb747608b5157117f11e175f113dab)) +* Post rebase fixes ([01483c4](https://github.com/propeller-heads/tycho-execution/commit/01483c4407d048e8225944b6218d6115de214669)) +* Prevent multiple callbacks ([f91b101](https://github.com/propeller-heads/tycho-execution/commit/f91b101a94c85b78b5b3033915c74649d6916070)) +* properly add ekubo_v2 to constants ([24dd814](https://github.com/propeller-heads/tycho-execution/commit/24dd814098768f8b3abf8c7a9f2dba091199bb90)) +* Remove --ignore-compile flag for slither ([353667e](https://github.com/propeller-heads/tycho-execution/commit/353667e56506fe042c82098eca6d55b4d0714b01)) +* Remove router_address from Solution object ([9eb18f4](https://github.com/propeller-heads/tycho-execution/commit/9eb18f4474f82c8af65c9da75580184e25e87e57)) +* Rename constants and update docstrings for clarity ([11dffdc](https://github.com/propeller-heads/tycho-execution/commit/11dffdcb2861499901b714a79a308185cb6e4041)) +* test fix after rebase ([e05ffed](https://github.com/propeller-heads/tycho-execution/commit/e05ffedd5dd38473677d465f50afed40b222422a)) +* Test+formatting fixes after rebase. ([6cf0f52](https://github.com/propeller-heads/tycho-execution/commit/6cf0f523c1c2f474574860c3d1c313f0ec2d5f98)) +* TransferType renaming after rebase ([12c410b](https://github.com/propeller-heads/tycho-execution/commit/12c410bb03853acecc6255f3089e988b416917a1)) +* unsupported protocols for chained swaps are always unsupported ([9ce6fc0](https://github.com/propeller-heads/tycho-execution/commit/9ce6fc015334f91e50b9ec4597c383eba3ad006c)) +* USV3 encoding/decoding after rebase ([2d0a3ac](https://github.com/propeller-heads/tycho-execution/commit/2d0a3ac3fd82ac45086f402916c7591e09b616f9)) + +## [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) diff --git a/Cargo.lock b/Cargo.lock index 09887cc..372c275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4317,9 +4317,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tycho-common" -version = "0.64.1" +version = "0.66.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e318a43fab79199deaab2391c83c75724780151c0337b67914ed835ff04b52f" +checksum = "5131fdb21cbd754822b0947fc6c763494531837ba8bb34123f6c7f4f89cb69f7" dependencies = [ "anyhow", "async-trait", @@ -4341,7 +4341,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.78.1" +version = "1.0.1" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 27c26a8..15dc129 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.78.1" +version = "1.0.1" 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] diff --git a/config/executor_addresses.json b/config/executor_addresses.json index 89ebca7..4faa439 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -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", diff --git a/foundry/scripts/deploy-executors.js b/foundry/scripts/deploy-executors.js index fa32960..9aa242f 100644 --- a/foundry/scripts/deploy-executors.js +++ b/foundry/scripts/deploy-executors.js @@ -4,84 +4,123 @@ 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: [ - "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", - "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" - ]}, - // SUSHISWAP - Args: Factory, Pool Init Code Hash - {exchange: "UniswapV2Executor", args: [ - "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac", - "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" - ]}, - // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash - {exchange: "UniswapV2Executor", args: [ - "0x1097053Fd2ea711dad45caCcc45EfF7548fCB362", - "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d" - ]}, - // USV3 -Args: Factory, Pool Init Code Hash - {exchange: "UniswapV3Executor", args: [ - "0x1F98431c8aD98523631AE4a59f267346ea31F984", - "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" - ]}, - // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash - {exchange: "UniswapV3Executor", args: [ - "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", - "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" - ]}, - // Args: Pool manager - {exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]}, - {exchange: "BalancerV2Executor", args: []}, - // Args: Ekubo core contract - {exchange: "EkuboExecutor", args: [ - "0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444" - ]} - ], - "base":[ - // Args: Factory, Pool Init Code Hash - {exchange: "UniswapV2Executor", args: [ - "0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6", - "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" - ]}, - // SUSHISWAP V2 - Args: Factory, Pool Init Code Hash - {exchange: "UniswapV2Executor", args: [ - "0x71524B4f93c58fcbF659783284E38825f0622859", - "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" - ]}, - // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash - {exchange: "UniswapV2Executor", args: [ - "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", - "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d" - ]}, - // USV3 - Args: Factory, Pool Init Code Hash - {exchange: "UniswapV3Executor", args: [ - "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", - "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" - ]}, - // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash - {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: [ - "0x1f98400000000000000000000000000000000002", - "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" - ]}, - // USV3 - Args: Factory, Pool Init Code Hash - {exchange: "UniswapV3Executor", args: [ - "0x1f98400000000000000000000000000000000003", - "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" - ]}, - // Args: Pool manager - {exchange: "UniswapV4Executor", args: ["0x1f98400000000000000000000000000000000004"]}, - ], + "ethereum": [ + // USV2 - Args: Factory, Pool Init Code Hash, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f", + 30 + ] + }, + // SUSHISWAP - Args: Factory, Pool Init Code Hash, Fee BPS, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac", + "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303", + 30 + ] + }, + // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0x1097053Fd2ea711dad45caCcc45EfF7548fCB362", + "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d", + 25 + ] + }, + // USV3 -Args: Factory, Pool Init Code Hash + { + exchange: "UniswapV3Executor", args: [ + "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" + ] + }, + // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash + { + exchange: "UniswapV3Executor", args: [ + "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" + ] + }, + // Args: Pool manager + {exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]}, + {exchange: "BalancerV2Executor", args: []}, + // Args: Ekubo core contract + { + exchange: "EkuboExecutor", args: [ + "0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444" + ] + }, + // Args: ETH address in curve pools + { + exchange: "CurveExecutor", args: [ + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + ] + } + ], + "base": [ + // Args: Factory, Pool Init Code Hash, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6", + "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f", + 30 + ] + }, + // SUSHISWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0x71524B4f93c58fcbF659783284E38825f0622859", + "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303", + 30 + ] + }, + // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", + "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d", + 25 + ] + }, + // USV3 - Args: Factory, Pool Init Code Hash + { + exchange: "UniswapV3Executor", args: [ + "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", + "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" + ] + }, + // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash + { + exchange: "UniswapV3Executor", args: [ + "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" + ] + }, + // Args: Pool manager + {exchange: "UniswapV4Executor", args: ["0x498581ff718922c3f8e6a244956af099b2652b2b"]}, + {exchange: "BalancerV2Executor", args: []}, + ], + "unichain": [ + // Args: Factory, Pool Init Code Hash, Fee BPS + { + exchange: "UniswapV2Executor", args: [ + "0x1f98400000000000000000000000000000000002", + "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f", + 30 + ] + }, + // USV3 - Args: Factory, Pool Init Code Hash + { + exchange: "UniswapV3Executor", args: [ + "0x1f98400000000000000000000000000000000003", + "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" + ] + }, + // Args: Pool manager + {exchange: "UniswapV4Executor", args: ["0x1f98400000000000000000000000000000000004"]}, + ], } async function main() { diff --git a/foundry/src/executors/BalancerV2Executor.sol b/foundry/src/executors/BalancerV2Executor.sol index 817c98b..00b772a 100644 --- a/foundry/src/executors/BalancerV2Executor.sol +++ b/foundry/src/executors/BalancerV2Executor.sol @@ -48,7 +48,7 @@ contract BalancerV2Executor is IExecutor, TokenTransfer { 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({ diff --git a/foundry/src/executors/CurveExecutor.sol b/foundry/src/executors/CurveExecutor.sol index 8c9995c..ef2af90 100644 --- a/foundry/src/executors/CurveExecutor.sol +++ b/foundry/src/executors/CurveExecutor.sol @@ -81,7 +81,7 @@ contract CurveExecutor is IExecutor, TokenTransfer { 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 diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 863216d..dd04dd1 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -10,6 +10,7 @@ error UniswapV2Executor__InvalidDataLength(); error UniswapV2Executor__InvalidTarget(); error UniswapV2Executor__InvalidFactory(); error UniswapV2Executor__InvalidInitCode(); +error UniswapV2Executor__InvalidFee(); contract UniswapV2Executor is IExecutor, TokenTransfer { using SafeERC20 for IERC20; @@ -17,10 +18,14 @@ contract UniswapV2Executor is IExecutor, TokenTransfer { address public immutable factory; bytes32 public immutable initCode; address private immutable self; + uint256 public immutable feeBps; - constructor(address _factory, bytes32 _initCode, address _permit2) - TokenTransfer(_permit2) - { + constructor( + address _factory, + bytes32 _initCode, + address _permit2, + uint256 _feeBps + ) TokenTransfer(_permit2) { if (_factory == address(0)) { revert UniswapV2Executor__InvalidFactory(); } @@ -29,6 +34,10 @@ contract UniswapV2Executor is IExecutor, TokenTransfer { } factory = _factory; initCode = _initCode; + if (_feeBps > 30) { + revert UniswapV2Executor__InvalidFee(); + } + feeBps = _feeBps; self = address(this); } @@ -100,9 +109,9 @@ contract UniswapV2Executor is IExecutor, TokenTransfer { } 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; } diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index 4609bc1..c1d570d 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -129,13 +129,7 @@ contract UniswapV3Executor is IExecutor, ICallback, TokenTransfer { 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) diff --git a/foundry/test/TychoRouterSingleSwap.t.sol b/foundry/test/TychoRouterSingleSwap.t.sol index 16b7f0c..b77e67e 100644 --- a/foundry/test/TychoRouterSingleSwap.t.sol +++ b/foundry/test/TychoRouterSingleSwap.t.sol @@ -367,4 +367,22 @@ contract TychoRouterSingleSwapTest is TychoRouterTestSetup { 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); + // Encoded solution generated using `test_single_swap_strategy_encoder_no_transfer_in` + (bool success,) = tychoRouterAddr.call( + hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae3740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + assertTrue(success, "Call Failed"); + assertEq(balanceAfter - balanceBefore, 2659881924818443699787); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index de68184..8aa79e9 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -99,7 +99,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper { IPoolManager poolManager = IPoolManager(poolManagerAddress); usv2Executor = - new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS); + new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS, 30); usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS); usv4Executor = new UniswapV4Executor(poolManager, PERMIT2_ADDRESS); diff --git a/foundry/test/executors/CurveExecutor.t.sol b/foundry/test/executors/CurveExecutor.t.sol index 278413c..629ce79 100644 --- a/foundry/test/executors/CurveExecutor.t.sol +++ b/foundry/test/executors/CurveExecutor.t.sol @@ -254,15 +254,15 @@ contract CurveExecutorTest is Test, Constants { 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, ALICE); + _getData(USDT_ADDR, CRVUSD_ADDR, CRVUSD_USDT_POOL, 1, ALICE); uint256 amountOut = curveExecutorExposed.swap(amountIn, data); - assertEq(amountOut, 999910); - assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), amountOut); + assertEq(amountOut, 10436946786333182306400100); + assertEq(IERC20(CRVUSD_ADDR).balanceOf(ALICE), amountOut); } function testMetaPool() public { diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index 64be1d2..ea0d825 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -8,9 +8,12 @@ import {Constants} from "../Constants.sol"; import {Permit2TestHelper} from "../Permit2TestHelper.sol"; contract UniswapV2ExecutorExposed is UniswapV2Executor { - constructor(address _factory, bytes32 _initCode, address _permit2) - UniswapV2Executor(_factory, _initCode, _permit2) - {} + constructor( + address _factory, + bytes32 _initCode, + address _permit2, + uint256 _feeBps + ) UniswapV2Executor(_factory, _initCode, _permit2, _feeBps) {} function decodeParams(bytes calldata data) external @@ -63,17 +66,19 @@ contract UniswapV2ExecutorTest is Test, Constants, Permit2TestHelper { uint256 forkBlock = 17323404; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); uniswapV2Exposed = new UniswapV2ExecutorExposed( - USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS + USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS, 30 ); sushiswapV2Exposed = new UniswapV2ExecutorExposed( SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH, - PERMIT2_ADDRESS + PERMIT2_ADDRESS, + 30 ); pancakeswapV2Exposed = new UniswapV2ExecutorExposed( PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH, - PERMIT2_ADDRESS + PERMIT2_ADDRESS, + 25 ); permit2 = IAllowanceTransfer(PERMIT2_ADDRESS); } diff --git a/foundry/test/executors/UniswapV3Executor.t.sol b/foundry/test/executors/UniswapV3Executor.t.sol index cd77697..e9d95e6 100644 --- a/foundry/test/executors/UniswapV3Executor.t.sol +++ b/foundry/test/executors/UniswapV3Executor.t.sol @@ -96,6 +96,29 @@ contract UniswapV3ExecutorTest is Test, Constants, Permit2TestHelper { ); } + 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 { bytes memory invalidParams = abi.encodePacked(WETH_ADDR, address(2), address(3)); diff --git a/src/encoding/evm/encoder_builders.rs b/src/encoding/evm/encoder_builders.rs index 94929a7..ee412ac 100644 --- a/src/encoding/evm/encoder_builders.rs +++ b/src/encoding/evm/encoder_builders.rs @@ -21,6 +21,7 @@ pub struct TychoRouterEncoderBuilder { chain: Option, executors_file_path: Option, router_address: Option, + token_in_already_in_router: Option, } impl Default for TychoRouterEncoderBuilder { @@ -36,6 +37,7 @@ impl TychoRouterEncoderBuilder { chain: None, executors_file_path: None, router_address: None, + token_in_already_in_router: None, } } pub fn chain(mut self, chain: TychoCommonChain) -> Self { @@ -62,6 +64,16 @@ impl TychoRouterEncoderBuilder { 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, EncodingError> { @@ -88,6 +100,8 @@ impl TychoRouterEncoderBuilder { swap_encoder_registry, self.swapper_pk, tycho_router_address, + self.token_in_already_in_router + .unwrap_or(false), )?)) } else { Err(EncodingError::FatalError( diff --git a/src/encoding/evm/group_swaps.rs b/src/encoding/evm/group_swaps.rs index 1409b9f..5ec5fe6 100644 --- a/src/encoding/evm/group_swaps.rs +++ b/src/encoding/evm/group_swaps.rs @@ -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, 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, pub split: f64, @@ -44,7 +44,7 @@ pub fn group_swaps(swaps: Vec) -> Vec { 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) -> Vec { 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, } @@ -304,15 +304,15 @@ mod tests { vec![ SwapGroup { swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], - input_token: weth.clone(), - output_token: usdc.clone(), + 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, } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index a9599c7..1148585 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -8,7 +8,6 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, - constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS}, group_swaps::group_swaps, strategy_encoder::{ strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, @@ -32,17 +31,15 @@ use crate::encoding::{ /// * `permit2`: Permit2, responsible for managing permit2 operations and providing necessary /// signatures and permit2 objects for calling the router /// * `selector`: String, the selector for the swap function in the router contract -/// * `native_address`: Address of the chain's native token -/// * `wrapped_address`: Address of the chain's wrapped token /// * `router_address`: Address of the router to be used to execute swaps +/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers #[derive(Clone)] pub struct SingleSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, permit2: Option, selector: String, - native_address: Bytes, - wrapped_address: Bytes, router_address: Bytes, + transfer_optimization: TransferOptimization, } impl SingleSwapStrategyEncoder { @@ -51,6 +48,7 @@ impl SingleSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) @@ -60,13 +58,19 @@ impl SingleSwapStrategyEncoder { "singleSwap(uint256,address,address,uint256,bool,bool,address,bytes)".to_string(), ) }; + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, swap_encoder_registry, - native_address: chain.native_token()?, - wrapped_address: chain.wrapped_token()?, - router_address, + router_address: router_address.clone(), + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + token_in_already_in_router, + router_address, + ), }) } @@ -80,8 +84,6 @@ impl SingleSwapStrategyEncoder { } } -impl TransferOptimization for SingleSwapStrategyEncoder {} - impl StrategyEncoder for SingleSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { let grouped_swaps = group_swaps(solution.clone().swaps); @@ -125,26 +127,20 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { let swap_receiver = if !unwrap { solution.receiver.clone() } else { self.router_address.clone() }; + let transfer_type = self + .transfer_optimization + .get_transfer_type(grouped_swap.clone(), solution.given_token.clone(), wrap, false); + let encoding_context = EncodingContext { + receiver: swap_receiver.clone(), + exact_out: solution.exact_out, + router_address: Some(self.router_address.clone()), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer_type.clone(), + }; + let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self.get_transfer_type( - swap.clone(), - solution.given_token.clone(), - self.native_address.clone(), - self.wrapped_address.clone(), - self.permit2.clone().is_some(), - wrap, - false, - ); - - let encoding_context = EncodingContext { - receiver: swap_receiver.clone(), - exact_out: solution.exact_out, - router_address: Some(self.router_address.clone()), - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - transfer_type: transfer_type.clone(), - }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); } @@ -215,6 +211,7 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { /// * `router_address`: Address of the router to be used to execute swaps /// * `sequential_swap_validator`: SequentialSwapValidator, responsible for checking validity of /// sequential swap solutions +/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers #[derive(Clone)] pub struct SequentialSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, @@ -224,16 +221,16 @@ pub struct SequentialSwapStrategyEncoder { native_address: Bytes, wrapped_address: Bytes, sequential_swap_validator: SequentialSwapValidator, + transfer_optimization: TransferOptimization, } -impl TransferOptimization for SequentialSwapStrategyEncoder {} - impl SequentialSwapStrategyEncoder { pub fn new( chain: Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "sequentialSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) @@ -244,14 +241,22 @@ impl SequentialSwapStrategyEncoder { .to_string(), ) }; + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, swap_encoder_registry, - router_address, + router_address: router_address.clone(), native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, sequential_swap_validator: SequentialSwapValidator, + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + token_in_already_in_router, + router_address, + ), }) } @@ -291,7 +296,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { } let mut swaps = vec![]; - let mut next_in_between_swap_optimization = true; + let mut next_in_between_swap_optimization_allowed = true; for (i, grouped_swap) in grouped_swaps.iter().enumerate() { let protocol = grouped_swap.protocol_system.clone(); let swap_encoder = self @@ -303,50 +308,31 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { )) })?; - let in_between_swap_optimization = next_in_between_swap_optimization; + let in_between_swap_optimization_allowed = next_in_between_swap_optimization_allowed; let next_swap = grouped_swaps.get(i + 1); - // if there is a next swap - let swap_receiver = 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()) { - next_in_between_swap_optimization = false; - self.router_address.clone() - } else { - Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| { - EncodingError::FatalError("Invalid component id".to_string()) - })? - } - } else { - // the protocol of the next swap does not support transfer in optimization - self.router_address.clone() - } - } else { - solution.receiver.clone() // last swap - there is not next swap + let (swap_receiver, next_swap_optimization) = self + .transfer_optimization + .get_receiver(solution.receiver.clone(), next_swap)?; + next_in_between_swap_optimization_allowed = next_swap_optimization; + let transfer_type = self + .transfer_optimization + .get_transfer_type( + grouped_swap.clone(), + solution.given_token.clone(), + wrap, + in_between_swap_optimization_allowed, + ); + let encoding_context = EncodingContext { + receiver: swap_receiver.clone(), + exact_out: solution.exact_out, + router_address: Some(self.router_address.clone()), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer_type.clone(), }; let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self.get_transfer_type( - swap.clone(), - solution.given_token.clone(), - self.native_address.clone(), - self.wrapped_address.clone(), - self.permit2.clone().is_some(), - wrap, - in_between_swap_optimization, - ); - - let encoding_context = EncodingContext { - receiver: swap_receiver.clone(), - exact_out: solution.exact_out, - router_address: Some(self.router_address.clone()), - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - transfer_type: transfer_type.clone(), - }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); @@ -422,6 +408,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { /// * `split_swap_validator`: SplitSwapValidator, responsible for checking validity of split swap /// solutions /// * `router_address`: Address of the router to be used to execute swaps +/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers #[derive(Clone)] pub struct SplitSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, @@ -431,6 +418,7 @@ pub struct SplitSwapStrategyEncoder { wrapped_address: Bytes, split_swap_validator: SplitSwapValidator, router_address: Bytes, + transfer_optimization: TransferOptimization, } impl SplitSwapStrategyEncoder { @@ -439,6 +427,7 @@ impl SplitSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "splitSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) @@ -449,7 +438,7 @@ impl SplitSwapStrategyEncoder { .to_string(), ) }; - + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, @@ -457,7 +446,14 @@ impl SplitSwapStrategyEncoder { native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, split_swap_validator: SplitSwapValidator, - router_address, + router_address: router_address.clone(), + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + token_in_already_in_router, + router_address, + ), }) } @@ -481,8 +477,6 @@ impl SplitSwapStrategyEncoder { } } -impl TransferOptimization for SplitSwapStrategyEncoder {} - impl StrategyEncoder for SplitSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.split_swap_validator @@ -513,7 +507,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { let intermediary_tokens: HashSet = grouped_swaps .iter() .flat_map(|grouped_swap| { - vec![grouped_swap.input_token.clone(), grouped_swap.output_token.clone()] + vec![grouped_swap.token_in.clone(), grouped_swap.token_out.clone()] }) .collect(); let mut intermediary_tokens: Vec = intermediary_tokens @@ -558,40 +552,33 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { )) })?; - let swap_receiver = if !unwrap && grouped_swap.output_token == solution.checked_token { + let swap_receiver = if !unwrap && grouped_swap.token_out == solution.checked_token { solution.receiver.clone() } else { self.router_address.clone() }; + let transfer_type = self + .transfer_optimization + .get_transfer_type(grouped_swap.clone(), solution.given_token.clone(), wrap, false); + let encoding_context = EncodingContext { + receiver: swap_receiver.clone(), + exact_out: solution.exact_out, + router_address: Some(self.router_address.clone()), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer_type.clone(), + }; let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self.get_transfer_type( - swap.clone(), - solution.given_token.clone(), - self.native_address.clone(), - self.wrapped_address.clone(), - self.permit2.clone().is_some(), - wrap, - false, - ); - - let encoding_context = EncodingContext { - receiver: swap_receiver.clone(), - exact_out: solution.exact_out, - router_address: Some(self.router_address.clone()), - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - transfer_type: transfer_type.clone(), - }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); } let swap_data = self.encode_swap_header( - get_token_position(tokens.clone(), grouped_swap.input_token.clone())?, - get_token_position(tokens.clone(), grouped_swap.output_token.clone())?, + get_token_position(tokens.clone(), grouped_swap.token_in.clone())?, + get_token_position(tokens.clone(), grouped_swap.token_out.clone())?, percentage_to_uint24(grouped_swap.split), Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) @@ -744,6 +731,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -827,6 +815,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -876,6 +865,85 @@ mod tests { println!("test_single_swap_strategy_encoder_no_permit2: {}", hex_calldata); } + #[test] + fn test_single_swap_strategy_encoder_no_transfer_in() { + // Performs a single swap from WETH to DAI on a USV2 pool assuming that the tokens are + // already in the router + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let expected_amount = Some(BigUint::from_str("2_650_000000000000000000").unwrap()); + let slippage = Some(0.01f64); + let checked_amount = Some(BigUint::from_str("2_640_000000000000000000").unwrap()); + let expected_min_amount = U256::from_str("2_640_000000000000000000").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + true, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount, + slippage, + checked_amount, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "20144a07", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "0000000000000000000000000000000000000000000000000000000000000100", // offset of swap bytes + "0000000000000000000000000000000000000000000000000000000000000052", // length of swap bytes without padding + + // Swap data + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "00", // zero2one + "00", // transfer type + "0000000000000000000000000000", // padding + ] + .join(""); + + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata, expected_input); + println!("test_single_swap_strategy_encoder_no_transfer_in: {}", hex_calldata); + } + #[test] fn test_single_swap_strategy_encoder_wrap() { // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH @@ -904,6 +972,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -956,6 +1025,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -1027,6 +1097,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1086,6 +1157,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1204,6 +1276,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -1321,6 +1394,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1402,6 +1476,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1487,6 +1562,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1553,6 +1629,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1800,6 +1877,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -1909,6 +1987,7 @@ mod tests { swap_encoder_registry, Some(private_key.clone()), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2071,6 +2150,7 @@ mod tests { swap_encoder_registry, Some(private_key.clone()), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2196,6 +2276,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap(), + false, ) .unwrap(); @@ -2261,6 +2342,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2329,6 +2411,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2416,6 +2499,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -2518,6 +2602,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); @@ -2580,6 +2665,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs index 97e08fc..62bfca1 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -1,50 +1,86 @@ +use std::str::FromStr; + use tycho_common::Bytes; use crate::encoding::{ - evm::constants::IN_TRANSFER_REQUIRED_PROTOCOLS, - models::{Swap, TransferType}, + errors::EncodingError, + evm::{ + constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS}, + group_swaps::SwapGroup, + }, + models::TransferType, }; -/// A trait that defines how the tokens will be transferred into the given pool given the solution. -pub trait TransferOptimization { - /// Returns the transfer method that should be used for the given swap and solution. - #[allow(clippy::too_many_arguments)] - fn get_transfer_type( - &self, - swap: Swap, - given_token: Bytes, +/// 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.component.protocol_system.as_str()); + IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.protocol_system.as_str()); let is_first_swap = swap.token_in == given_token; - if swap.token_in == native_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 == wrapped_token) && wrap { + } 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 permit2 { + 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 } - } else if permit2 { - // Transfer from swapper to router using permit2. - TransferType::TransferPermit2ToRouter + // 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 { - // Transfer from swapper to router. - TransferType::TransferFromToRouter + TransferType::None } // all other swaps } else if !in_transfer_required || in_between_swap_optimization { @@ -54,17 +90,50 @@ pub trait TransferOptimization { 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 tycho_common::{models::protocol::ProtocolComponent, Bytes}; + use rstest::rstest; + use tycho_common::models::protocol::ProtocolComponent; use super::*; - - struct MockStrategy {} - impl TransferOptimization for MockStrategy {} + use crate::encoding::models::Swap; fn weth() -> Bytes { Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) @@ -82,39 +151,37 @@ mod tests { 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 = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: weth(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), true, false, false); + 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 = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: weth(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false); + 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); } @@ -122,18 +189,15 @@ mod tests { 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 = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: eth(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, false, false); + 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); } @@ -141,18 +205,15 @@ mod tests { 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 = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: weth(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, true, false); + 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); } @@ -160,18 +221,15 @@ mod tests { 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 = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: usdc(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false); + 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); } @@ -179,18 +237,15 @@ mod tests { 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 = Swap { - component: ProtocolComponent { - protocol_system: "vm:curve".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "vm:curve".to_string(), token_in: usdc(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false); + 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); } @@ -198,18 +253,100 @@ mod tests { 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 = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: usdc(), token_out: dai(), split: 0f64, + swaps: vec![], }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, true); + 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); + } } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index e0db5de..854509f 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -378,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 { @@ -450,10 +451,34 @@ 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)) + } + } } } } @@ -483,6 +508,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, }) } @@ -536,7 +562,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)?; @@ -1210,6 +1239,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, diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 5d285e0..a4bbc7a 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -39,6 +39,7 @@ impl TychoRouterEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let native_address = chain.native_token()?; let wrapped_address = chain.wrapped_token()?; @@ -48,18 +49,21 @@ impl TychoRouterEncoder { swap_encoder_registry.clone(), swapper_pk.clone(), router_address.clone(), + token_in_already_in_router, )?, sequential_swap_strategy: SequentialSwapStrategyEncoder::new( chain.clone(), swap_encoder_registry.clone(), swapper_pk.clone(), router_address.clone(), + token_in_already_in_router, )?, split_swap_strategy: SplitSwapStrategyEncoder::new( chain, swap_encoder_registry, None, router_address.clone(), + token_in_already_in_router, )?, native_address, wrapped_address, @@ -258,8 +262,8 @@ impl TychoExecutorEncoder { receiver: receiver.clone(), exact_out: solution.exact_out, router_address: None, - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), transfer_type: TransferType::TransferToProtocol, }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; @@ -354,6 +358,7 @@ mod tests { get_swap_encoder_registry(), None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap() }