diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e854a..6f1fa06 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/Cargo.lock b/Cargo.lock index 09887cc..7cce6e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,13 +4,19 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -24,10 +30,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.16", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -71,13 +77,13 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.1.55" +version = "0.1.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e39f295f876b61a1222d937e1dd31f965e4a1acc3bba98e448dd7e84b1a4566" +checksum = "28e2652684758b0d9b389d248b209ed9fd9989ef489a550265fe4bb8454fe7eb" dependencies = [ "alloy-primitives", "num_enum", - "strum 0.26.3", + "strum 0.27.1", ] [[package]] @@ -93,7 +99,7 @@ dependencies = [ "alloy-trie", "auto_impl", "c-kzg", - "derive_more", + "derive_more 1.0.0", "serde", ] @@ -113,9 +119,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648275bb59110f88cc5fa9a176845e52a554ebfebac2d21220bcda8c9220f797" +checksum = "9d8bcce99ad10fe02640cfaec1c6bc809b837c783c1d52906aa5af66e2a196f6" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -126,16 +132,16 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9138f4f0912793642d453523c3116bd5d9e11de73b70177aa7cb3e94b98ad2" +checksum = "eb8e762aefd39a397ff485bc86df673465c4ad3ec8819cc60833a8a3ba5cdc87" dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-type-parser", "alloy-sol-types", "const-hex", - "derive_more", + "derive_more 2.0.1", "itoa", "serde", "serde_json", @@ -155,14 +161,14 @@ dependencies = [ [[package]] name = "alloy-eip7702" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabf647eb4650c91a9d38cb6f972bb320009e7e9d61765fb688a86f1563b33e8" +checksum = "9b15b13d38b366d01e818fe8e710d4d702ef7499eacd44926a06171dd9585d0c" dependencies = [ "alloy-primitives", "alloy-rlp", - "derive_more", "serde", + "thiserror 2.0.12", ] [[package]] @@ -177,7 +183,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "c-kzg", - "derive_more", + "derive_more 1.0.0", "once_cell", "serde", "sha2", @@ -198,9 +204,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24acd2f5ba97c7a320e67217274bc81fe3c3174b8e6144ec875d9d54e760e278" +checksum = "fe6beff64ad0aa6ad1019a3db26fef565aefeb011736150ab73ed3366c3cfd1b" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -218,7 +224,7 @@ dependencies = [ "alloy-sol-types", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -244,7 +250,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -262,24 +268,24 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec878088ec6283ce1e90d280316aadd3d6ce3de06ff63d68953c855e7e447e92" +checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", - "derive_more", + "derive_more 2.0.1", "foldhash", "hashbrown 0.15.2", - "indexmap 2.7.0", + "indexmap 2.9.0", "itoa", "k256", "keccak-asm", "paste", "proptest", - "rand", + "rand 0.8.5", "ruint", "rustc-hash", "serde", @@ -312,12 +318,12 @@ dependencies = [ "futures-utils-wasm", "lru", "parking_lot", - "pin-project 1.1.8", + "pin-project 1.1.10", "reqwest", "schnellru", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "url", @@ -326,9 +332,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f542548a609dca89fcd72b3b9f355928cf844d4363c5eed9c5273a3dd225e097" +checksum = "3d6c1d995bff8d011f7cd6c81820d51825e6e06d6db73914c1630ecf544d83d6" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -337,13 +343,13 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a833d97bf8a5f0f878daf2c8451fff7de7f9de38baa5a45d936ec718d81255a" +checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -357,7 +363,7 @@ dependencies = [ "alloy-transport", "alloy-transport-http", "futures", - "pin-project 1.1.8", + "pin-project 1.1.10", "reqwest", "serde", "serde_json", @@ -409,7 +415,7 @@ dependencies = [ "itertools 0.13.0", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -436,7 +442,7 @@ dependencies = [ "auto_impl", "elliptic-curve", "k256", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -453,7 +459,7 @@ dependencies = [ "aws-sdk-kms", "k256", "spki", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -471,7 +477,7 @@ dependencies = [ "gcloud-sdk", "k256", "spki", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -490,8 +496,8 @@ dependencies = [ "async-trait", "coins-ledger", "futures-util", - "semver 1.0.24", - "thiserror 2.0.11", + "semver 1.0.26", + "thiserror 2.0.12", "tracing", ] @@ -507,62 +513,63 @@ dependencies = [ "alloy-signer", "async-trait", "k256", - "rand", - "thiserror 2.0.11", + "rand 0.8.5", + "thiserror 2.0.12", ] [[package]] name = "alloy-sol-macro" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d039d267aa5cbb7732fa6ce1fd9b5e9e29368f580f80ba9d7a8450c794de4b2" +checksum = "e10ae8e9a91d328ae954c22542415303919aabe976fe7a92eb06db1b68fd59f2" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "620ae5eee30ee7216a38027dec34e0585c55099f827f92f50d11e3d2d3a4a954" +checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" dependencies = [ "alloy-sol-macro-input", "const-hex", "heck 0.5.0", - "indexmap 2.7.0", + "indexmap 2.9.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9f7d057e00f8c5994e4ff4492b76532c51ead39353aa2ed63f8c50c0f4d52e" +checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" dependencies = [ "const-hex", "dunce", "heck 0.5.0", + "macro-string", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e60b084fe1aef8acecda2743ff2d93c18ff3eb67a2d3b12f62582a1e66ef5e" +checksum = "6d162f8524adfdfb0e4bd0505c734c985f3e2474eb022af32eef0d52a4f3935c" dependencies = [ "serde", "winnow", @@ -570,9 +577,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1382302752cd751efd275f4d6ef65877ddf61e0e6f5ac84ef4302b79a33a31a" +checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -588,12 +595,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d17722a198f33bbd25337660787aea8b8f57814febb7c746bc30407bdfc39448" dependencies = [ "alloy-json-rpc", - "base64 0.22.1", + "base64", "futures-util", "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tower 0.5.2", "tracing", @@ -618,14 +625,14 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6917c79e837aa7b77b7a6dae9f89cbe15313ac161c4d3cfaf8909ef21f3d22d8" +checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b" dependencies = [ "alloy-primitives", "alloy-rlp", "arrayvec", - "derive_more", + "derive_more 1.0.0", "nybbles", "serde", "smallvec", @@ -699,9 +706,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "ark-ff" @@ -814,7 +821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -824,7 +831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -838,9 +845,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.18" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -868,18 +875,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -890,13 +897,13 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -907,9 +914,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-credential-types" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -919,9 +926,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac" +checksum = "0aff45ffe35196e593ea3b9dd65b320e51e2dda95aff4390bc459e461d09c6ad" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -944,9 +951,9 @@ dependencies = [ [[package]] name = "aws-sdk-kms" -version = "1.55.0" +version = "1.65.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011f0f9ebfaa2f76f0cddcc05a6951c74c226af8a493c56ef7ca1a880e1de4ac" +checksum = "f5325c5e2badf4148e850017cc56cc205888c6e0b52c9e29d3501ec577005230" dependencies = [ "aws-credential-types", "aws-runtime", @@ -958,6 +965,7 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "fastrand", "http 0.2.12", "once_cell", "regex-lite", @@ -966,9 +974,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05" +checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -979,7 +987,7 @@ dependencies = [ "hex", "hmac", "http 0.2.12", - "http 1.2.0", + "http 1.3.1", "once_cell", "percent-encoding", "sha2", @@ -989,9 +997,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" dependencies = [ "futures-util", "pin-project-lite", @@ -1000,9 +1008,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.12" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" +checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -1010,8 +1018,8 @@ dependencies = [ "bytes-utils", "futures-core", "http 0.2.12", + "http 1.3.1", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "pin-utils", @@ -1020,30 +1028,39 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.2" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422" +checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" dependencies = [ "aws-smithy-types", ] [[package]] -name = "aws-smithy-runtime" -version = "1.7.7" +name = "aws-smithy-observability" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865f7050bbc7107a6c98a397a9fcd9413690c27fa718446967cf03b2d3ac517e" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "fastrand", "http 0.2.12", + "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", - "httparse", - "once_cell", "pin-project-lite", "pin-utils", "tokio", @@ -1052,15 +1069,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" +checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57" dependencies = [ "aws-smithy-async", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.2.0", + "http 1.3.1", "pin-project-lite", "tokio", "tracing", @@ -1069,15 +1086,15 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.12" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28f6feb647fb5e0d5b50f0472c19a7db9462b74e2fec01bb0b44eedcc834e97" +checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3" dependencies = [ "base64-simd", "bytes", "bytes-utils", "http 0.2.12", - "http 1.2.0", + "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1092,9 +1109,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.4" +version = "1.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2" +checksum = "3873f8deed8927ce8d04487630dc9ff73193bab64742a61d050e57a68dec4125" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1114,7 +1131,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "itoa", @@ -1140,7 +1157,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", @@ -1153,17 +1170,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", - "windows-targets", ] [[package]] @@ -1172,12 +1189,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1196,24 +1207,24 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -1223,9 +1234,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bitvec" @@ -1250,9 +1261,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" dependencies = [ "cc", "glob", @@ -1262,15 +1273,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byte-slice-cast" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "byteorder" @@ -1280,9 +1291,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -1314,9 +1325,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.9" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -1335,9 +1346,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1345,14 +1356,14 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-link", ] [[package]] name = "clap" -version = "4.5.28" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -1360,9 +1371,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -1372,14 +1383,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -1398,7 +1409,7 @@ dependencies = [ "byteorder", "cfg-if", "const-hex", - "getrandom", + "getrandom 0.2.16", "hidapi-rusb", "js-sys", "log", @@ -1436,6 +1447,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1464,9 +1495,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1488,9 +1519,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -1499,7 +1530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1530,9 +1561,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -1541,9 +1572,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -1565,7 +1596,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -1576,7 +1616,18 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", "unicode-xid", ] @@ -1609,7 +1660,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -1640,9 +1691,9 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elliptic-curve" @@ -1658,7 +1709,7 @@ dependencies = [ "group", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1666,15 +1717,15 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" dependencies = [ "serde", "typeid", @@ -1682,9 +1733,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1720,11 +1771,11 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1735,19 +1786,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.8", ] [[package]] @@ -1758,9 +1809,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" @@ -1848,7 +1899,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -1934,22 +1985,36 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -1964,23 +2029,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.2.0", - "indexmap 2.7.0", + "http 1.3.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -2078,9 +2143,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -2105,27 +2170,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -2135,15 +2200,15 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", "h2", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", @@ -2161,7 +2226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.2.0", + "http 1.3.1", "hyper", "hyper-util", "rustls", @@ -2203,16 +2268,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2222,14 +2288,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -2284,9 +2351,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -2308,9 +2375,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -2329,9 +2396,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -2358,7 +2425,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2399,7 +2466,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2414,9 +2481,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2425,18 +2492,18 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b31349d02fe60f80bbbab1a9402364cad7460626d6030494b08ac4a2075bf81" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" dependencies = [ "rustversion", ] [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is_terminal_polyfill" @@ -2463,10 +2530,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.14" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" @@ -2480,11 +2556,11 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.0" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.21.7", + "base64", "js-sys", "pem", "ring", @@ -2533,15 +2609,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libusb1-sys" @@ -2557,15 +2633,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -2579,9 +2655,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" @@ -2592,6 +2668,17 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "matchit" version = "0.7.3" @@ -2631,9 +2718,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -2645,15 +2741,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -2742,7 +2838,7 @@ checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2760,26 +2856,26 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -2796,20 +2892,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -2825,28 +2921,30 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parity-scale-codec" -version = "3.6.12" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" dependencies = [ "arrayvec", "bitvec", "byte-slice-cast", + "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", + "rustversion", "serde", ] [[package]] name = "parity-scale-codec-derive" -version = "3.6.12" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" +checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] @@ -2869,7 +2967,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2880,11 +2978,11 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] @@ -2905,12 +3003,12 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] @@ -2925,11 +3023,11 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ - "pin-project-internal 1.1.8", + "pin-project-internal 1.1.10", ] [[package]] @@ -2945,13 +3043,13 @@ dependencies = [ [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -2978,9 +3076,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "powerfmt" @@ -2990,11 +3088,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.24", ] [[package]] @@ -3010,9 +3108,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] @@ -3060,31 +3158,31 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.8.0", + "bitflags 2.9.0", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -3094,9 +3192,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", "prost-derive", @@ -3104,22 +3202,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "prost-types" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ "prost", ] @@ -3132,37 +3230,39 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", - "getrandom", - "rand", + "getrandom 0.3.2", + "rand 0.9.1", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tracing", "web-time", @@ -3170,9 +3270,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" dependencies = [ "cfg_aliases", "libc", @@ -3184,13 +3284,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radium" version = "0.7.0" @@ -3204,11 +3310,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "serde", ] +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3216,7 +3332,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -3225,7 +3351,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", ] [[package]] @@ -3234,16 +3369,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -3289,16 +3424,16 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "async-compression", - "base64 0.22.1", + "base64", "bytes", "futures-core", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper", @@ -3349,15 +3484,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -3398,15 +3532,15 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.96", + "syn 2.0.100", "unicode-ident", ] [[package]] name = "ruint" -version = "1.12.4" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ef8fb1dd8de3870cb8400d51b4c2023854bbafd5431a3ac7e7317243e22d2f" +checksum = "78a46eb779843b2c4f21fac5773e25d6d5b7c8f0922876c91541790d2ca27eef" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -3420,7 +3554,8 @@ dependencies = [ "parity-scale-codec", "primitive-types", "proptest", - "rand", + "rand 0.8.5", + "rand 0.9.1", "rlp", "ruint-macro", "serde", @@ -3452,9 +3587,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc-hex" @@ -3477,16 +3612,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.24", + "semver 1.0.26", ] [[package]] name = "rustix" -version = "0.38.43" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -3495,9 +3630,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "log", "once_cell", @@ -3531,18 +3666,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -3551,9 +3686,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -3569,9 +3704,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" @@ -3632,7 +3767,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3645,7 +3780,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -3673,9 +3808,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "semver-parser" @@ -3688,29 +3823,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -3769,9 +3904,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -3783,7 +3918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3794,7 +3929,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -3809,29 +3944,23 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" version = "0.7.3" @@ -3868,11 +3997,11 @@ checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" dependencies = [ - "strum_macros 0.26.4", + "strum_macros 0.27.1", ] [[package]] @@ -3885,20 +4014,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "rustversion", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3920,9 +4049,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -3931,14 +4060,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84e4d83a0a6704561302b917a932484e1cae2d8c6354c64be8b7bac1c1fe057" +checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3958,7 +4087,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -3969,13 +4098,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.15.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.2", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3992,11 +4120,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -4007,18 +4135,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -4032,9 +4160,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -4047,15 +4175,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -4082,9 +4210,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -4097,9 +4225,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -4121,7 +4249,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -4136,9 +4264,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -4158,9 +4286,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -4177,11 +4305,11 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.9.0", "toml_datetime", "winnow", ] @@ -4195,17 +4323,17 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.22.1", + "base64", "bytes", "h2", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper", "hyper-timeout", "hyper-util", "percent-encoding", - "pin-project 1.1.8", + "pin-project 1.1.10", "prost", "rustls-native-certs", "rustls-pemfile", @@ -4228,9 +4356,9 @@ dependencies = [ "futures-core", "futures-util", "indexmap 1.9.3", - "pin-project 1.1.8", + "pin-project 1.1.10", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -4297,7 +4425,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -4317,16 +4445,16 @@ 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", "bytes", "chrono", "hex", - "rand", + "rand 0.8.5", "serde", "serde_json", "strum 0.25.0", @@ -4341,7 +4469,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.78.1" +version = "0.84.0" dependencies = [ "alloy", "alloy-primitives", @@ -4364,21 +4492,21 @@ dependencies = [ [[package]] name = "typeid" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typetag" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "044fc3365ddd307c297fe0fe7b2e70588cdab4d0f62dc52055ca0d11b174cf0e" +checksum = "73f22b40dd7bfe8c14230cf9702081366421890435b2d625fa92b4acc4c3de6f" dependencies = [ "erased-serde", "inventory", @@ -4389,13 +4517,13 @@ dependencies = [ [[package]] name = "typetag-impl" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9d30226ac9cbd2d1ff775f74e8febdab985dab14fb14aa2582c29a92d5555dc" +checksum = "35f5380909ffc31b4de4f4bdf96b877175a016aa2ca98cee39fcfd8c4d53d952" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -4430,9 +4558,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" @@ -4481,7 +4609,7 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.9.0", "serde", "serde_json", "utoipa-gen", @@ -4496,37 +4624,37 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "uuid" -version = "1.12.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom", - "rand", + "getrandom 0.3.2", + "rand 0.9.1", "serde", "uuid-macro-internal", ] [[package]] name = "uuid-macro-internal" -version = "1.12.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144b419c512fdd5eaa4c2998813e32aaab2b257746ee038de93985a99635501d" +checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -4548,9 +4676,9 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -4570,6 +4698,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -4592,7 +4729,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -4627,7 +4764,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4690,41 +4827,81 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", ] [[package]] -name = "windows-registry" -version = "0.2.0" +name = "windows-implement" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", - "windows-targets", + "windows-strings 0.3.1", + "windows-targets 0.53.0", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-result", - "windows-targets", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", ] [[package]] @@ -4733,7 +4910,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4742,7 +4919,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4751,14 +4928,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -4767,42 +4960,84 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4810,14 +5045,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.24" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -4859,7 +5109,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "synstructure", ] @@ -4869,8 +5119,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -4881,27 +5139,38 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", "synstructure", ] @@ -4922,7 +5191,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] [[package]] @@ -4944,5 +5213,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.100", ] diff --git a/Cargo.toml b/Cargo.toml index 27c26a8..ff9d729 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/README.md b/README.md index 5abdbea..eec77c7 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file 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/config/test_executor_addresses.json b/config/test_executor_addresses.json index 018a56b..8c6dc5c 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -7,7 +7,7 @@ "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", - "ekubo_v2": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7", + "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598", "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211" } } diff --git a/examples/encoding-example/main.rs b/examples/encoding-example/main.rs index 5edc965..2209258 100644 --- a/examples/encoding-example/main.rs +++ b/examples/encoding-example/main.rs @@ -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(); diff --git a/foundry/foundry.toml b/foundry/foundry.toml index 26d12dc..9acffad 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -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 \ No newline at end of file diff --git a/foundry/lib/LibSwap.sol b/foundry/lib/LibSwap.sol index f73a759..a892594 100644 --- a/foundry/lib/LibSwap.sol +++ b/foundry/lib/LibSwap.sol @@ -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:]; } } 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/Dispatcher.sol b/foundry/src/Dispatcher.sol index 828d0b1..1bb6209 100644 --- a/foundry/src/Dispatcher.sol +++ b/foundry/src/Dispatcher.sol @@ -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; } } diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 3a549cd..55be637 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -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); } } diff --git a/foundry/src/executors/BalancerV2Executor.sol b/foundry/src/executors/BalancerV2Executor.sol index 0f69fcd..00b772a 100644 --- a/foundry/src/executors/BalancerV2Executor.sol +++ b/foundry/src/executors/BalancerV2Executor.sol @@ -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])); } } diff --git a/foundry/src/executors/CurveExecutor.sol b/foundry/src/executors/CurveExecutor.sol index 569072f..ef2af90 100644 --- a/foundry/src/executors/CurveExecutor.sol +++ b/foundry/src/executors/CurveExecutor.sol @@ -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 { diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index 564b9d2..25d40ab 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -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 locked(uint256) external coreOnly { - int128 nextAmountIn = int128(uint128(bytes16(msg.data[36:52]))); - uint128 tokenInDebtAmount = uint128(nextAmountIn); + function handleCallback(bytes calldata raw) + external + returns (bytes memory) + { + verifyCallback(raw); - address receiver = address(bytes20(msg.data[52:72])); - address tokenIn = address(bytes20(msg.data[72:POOL_DATA_OFFSET])); + // Without selector and locker id + bytes calldata stripped = raw[36:]; - address nextTokenIn = tokenIn; + bytes4 selector = bytes4(raw[:4]); - 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; + 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(); } - _pay(tokenIn, tokenInDebtAmount); + return result; + } - core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn)); + function verifyCallback(bytes calldata) public view coreOnly {} + function locked(uint256) external coreOnly { + // 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 {} diff --git a/foundry/src/executors/TokenTransfer.sol b/foundry/src/executors/TokenTransfer.sol new file mode 100644 index 0000000..b5f8629 --- /dev/null +++ b/foundry/src/executors/TokenTransfer.sol @@ -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 + ); + } + } +} diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 50fb056..dd04dd1 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -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; } diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index 1d46c08..c1d570d 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -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( diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index a487ef1..f594adc 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -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)); + bytes memory result = poolManager.unlock(swapData); + uint128 amountOut = abi.decode(result, (uint128)); - 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); + 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); } - function msgSender() public view override returns (address) { - return address(this); + /** + * @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; + } + + /** + * @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; + } } } diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 51b292d..cb81d58 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -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 = diff --git a/foundry/test/Dispatcher.t.sol b/foundry/test/Dispatcher.t.sol index a7409fe..5841f26 100644 --- a/foundry/test/Dispatcher.t.sol +++ b/foundry/test/Dispatcher.t.sol @@ -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 { diff --git a/foundry/test/LibSwap.t.sol b/foundry/test/LibSwap.t.sol index 75c94f7..326df54 100644 --- a/foundry/test/LibSwap.t.sol +++ b/foundry/test/LibSwap.t.sol @@ -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); } } diff --git a/foundry/test/Permit2TestHelper.sol b/foundry/test/Permit2TestHelper.sol new file mode 100644 index 0000000..912b007 --- /dev/null +++ b/foundry/test/Permit2TestHelper.sol @@ -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); + } +} diff --git a/foundry/test/TestUtils.sol b/foundry/test/TestUtils.sol new file mode 100644 index 0000000..3399c9f --- /dev/null +++ b/foundry/test/TestUtils.sol @@ -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"); + } +} diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 49fde90..e85ad3d 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -10,8 +10,6 @@ import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; contract TychoRouterTest is TychoRouterTestSetup { bytes32 public constant EXECUTOR_SETTER_ROLE = 0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87; - bytes32 public constant FEE_SETTER_ROLE = - 0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060; bytes32 public constant PAUSER_ROLE = 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a; bytes32 public constant FUND_RESCUER_ROLE = @@ -133,32 +131,6 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testFeeSetting() public { - vm.startPrank(FEE_SETTER); - assertEq(tychoRouter.fee(), 0); - tychoRouter.setFee(100); - assertEq(tychoRouter.fee(), 100); - vm.stopPrank(); - - vm.startPrank(BOB); - vm.expectRevert(); - tychoRouter.setFee(200); - vm.stopPrank(); - } - - function testFeeReceiverSetting() public { - vm.startPrank(FEE_SETTER); - assertEq(tychoRouter.feeReceiver(), address(0)); - tychoRouter.setFeeReceiver(FEE_RECEIVER); - assertEq(tychoRouter.feeReceiver(), FEE_RECEIVER); - vm.stopPrank(); - - vm.startPrank(BOB); - vm.expectRevert(); - tychoRouter.setFeeReceiver(FEE_RECEIVER); - vm.stopPrank(); - } - function testPause() public { vm.startPrank(PAUSER); assertEq(tychoRouter.paused(), false); @@ -206,1273 +178,10 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSwapSimple() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // 1 WETH -> DAI - // (USV2) - uint256 amountIn = 1 ether; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); - assertEq(daiBalance, 2659881924818443699787); - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSwapSimplePermit2() 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, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - 2659881924818443699786, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, 2659881924818443699787); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); - - vm.stopPrank(); - } - - function testSwapMultipleHops() public { - // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 - // 1 WETH -> DAI -> USDC - // (univ2) (univ2) - uint256 amountIn = 1 ether; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - bytes[] memory swaps = new bytes[](2); - // WETH -> DAI - swaps[0] = encodeSwap( - uint8(0), - uint8(1), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ) - ); - - // DAI -> USDC - swaps[1] = encodeSwap( - uint8(1), - uint8(2), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) - ); - - tychoRouter.exposedSwap(amountIn, 3, pleEncode(swaps)); - - uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2644659787); - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSwapSplitHops() public { - // Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2 - // -> DAI -> - // 1 WETH USDC - // -> WBTC -> - // (univ2) (univ2) - uint256 amountIn = 1 ether; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - bytes[] memory swaps = new bytes[](4); - // WETH -> WBTC (60%) - swaps[0] = encodeSwap( - uint8(0), - uint8(1), - (0xffffff * 60) / 100, // 60% - address(usv2Executor), - encodeUniswapV2Swap( - WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false - ) - ); - // WBTC -> USDC - swaps[1] = encodeSwap( - uint8(1), - uint8(2), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap( - WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true - ) - ); - // WETH -> DAI - swaps[2] = encodeSwap( - uint8(0), - uint8(3), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ) - ); - - // DAI -> USDC - swaps[3] = encodeSwap( - uint8(3), - uint8(2), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) - ); - - tychoRouter.exposedSwap(amountIn, 4, pleEncode(swaps)); - - uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2615491639); - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSwapChecked() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Does permit2 token approval and transfer - // Checks amount out at the end - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 expectedAmount = 2659881924818443699787; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); - - vm.stopPrank(); - } - - function testSwapCheckedUndefinedMinAmount() 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); - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - uint256 minAmountOut = 0; - - vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); - tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - vm.stopPrank(); - } - - function testSwapCheckedNoPermit2() 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, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.swap( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - pleEncode(swaps) - ); - - uint256 expectedAmount = 2659881924818443699787; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); - - vm.stopPrank(); - } - - function testSwapCheckedLessApprovalFailure() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Fails while transferring the tokenIn to the router due to insufficient approval - 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 protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 minAmountOut = 2600 * 1e18; - vm.expectRevert(); - tychoRouter.swap( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - pleEncode(swaps) - ); - - vm.stopPrank(); - } - - function testSwapCheckedNegativeSlippageFailure() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Does permit2 token approval and transfer - // Checks amount out at the end and fails - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 minAmountOut = 3000 * 1e18; - vm.expectRevert( - abi.encodeWithSelector( - TychoRouter__NegativeSlippage.selector, - 2659881924818443699787, // actual amountOut - minAmountOut - ) - ); - tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - vm.stopPrank(); - } - - function testSwapFee() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Does permit2 token approval and transfer - // Takes fee at the end - - vm.startPrank(FEE_SETTER); - tychoRouter.setFee(100); - tychoRouter.setFeeReceiver(FEE_RECEIVER); - vm.stopPrank(); - - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 amountOut = tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - 2633283105570259262780, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 expectedAmount = 2633283105570259262790; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997); - - vm.stopPrank(); - } - - function testSwapWrapETH() 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, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 amountOut = tychoRouter.swapPermit2{value: amountIn}( - amountIn, - address(0), - DAI_ADDR, - 2659881924818443699780, - true, - false, - 2, - ALICE, - emptyPermitSingle, - "", - pleEncode(swaps) - ); - uint256 expectedAmount = 2659881924818443699787; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(ALICE.balance, 0); - - vm.stopPrank(); - } - - function testSwapUnwrapETH() 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, amountIn); - - bytes memory protocolData = - encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 amountOut = tychoRouter.swapPermit2( - amountIn, - DAI_ADDR, - address(0), - 1120007305574805920, - false, - true, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 expectedAmount = 1120007305574805922; // 1.12 ETH - assertEq(amountOut, expectedAmount); - assertEq(ALICE.balance, expectedAmount); - - vm.stopPrank(); - } - - function testSwapSingleUSV3() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V3 - // 1 WETH -> DAI - // (USV3) - uint256 amountIn = 10 ** 18; - deal(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, tychoRouterAddr, DAI_WETH_USV3, zeroForOne - ); - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - - uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); - assertGe(finalBalance, expAmountOut); - } - - function testSwapSinglePancakeV3() public { - // Trade 1 WETH for USDT with 1 swap on Pancakeswap V3 - // 1 WETH -> USDT - // (PancakeV3) - uint256 amountIn = 10 ** 18; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - uint256 expAmountOut = 2659_567519; //Swap 1 WETH for 1205.12 DAI - bool zeroForOne = true; - bytes memory protocolData = encodeUniswapV3Swap( - WETH_ADDR, - USDT_ADDR, - tychoRouterAddr, - PANCAKESWAPV3_WETH_USDT_POOL, - zeroForOne - ); - bytes memory swap = encodeSwap( - uint8(0), - uint8(1), - uint24(0), - address(pancakev3Executor), - protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - - uint256 finalBalance = IERC20(USDT_ADDR).balanceOf(tychoRouterAddr); - assertGe(finalBalance, expAmountOut); - } - - function testSwapSingleUSV3Permit2() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2 - // 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, amountIn); - - uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI - bool zeroForOne = false; - bytes memory protocolData = encodeUniswapV3Swap( - WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne - ); - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - expAmountOut - 1, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertGe(finalBalance, expAmountOut); - - vm.stopPrank(); - } - function testEmptySwapsRevert() public { uint256 amountIn = 10 ** 18; bytes memory swaps = ""; vm.expectRevert(TychoRouter__EmptySwaps.selector); - tychoRouter.exposedSwap(amountIn, 2, swaps); - } - - function testSingleSwapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Tests swapping WETH -> DAI on a USV2 pool - deal(WETH_ADDR, ALICE, 1 ether); - uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000681362ea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebdcf2000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a82e9bdde90314de4b1bf918cc2e8b27da98adcab46e8e99d4e77472a572d6381837e9453095f4cc5e9b25691b678288174e547e040a67d12b36ddfdd1e672d21b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" - ); - - vm.stopPrank(); - - uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 2659881924818443699787); - } - - function testSingleSwapWithoutPermit2Integration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Tests swapping WETH -> DAI on a USV2 pool without permit2 - deal(WETH_ADDR, ALICE, 1 ether); - vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(address(tychoRouterAddr), 1 ether); - uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_no_permit2` - (bool success,) = tychoRouterAddr.call( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae37400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" - ); - - vm.stopPrank(); - uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 2659881924818443699787); - } - - function testUSV4Integration() 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); - // Encoded solution generated using `test_split_encoding_strategy_usv4` - (bool success,) = tychoRouterAddr.call( - hex"d499aa88000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000006814875700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ed015f0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000413a7c6367c69ac46fc2b633fd53e583b74b20ec9b3ea83b069fe564765560a4cb335af200fd90ddb5f56d11e469c11a97420499f1b3ee0c1db13149a74daa90db1b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c008a0001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 97191013220606467325121599); - } - - function testUSV4IntegrationInputETH() 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); - - // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` - (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933000000000000000000000000000000000000000000c87c939ae635f92dc2379c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006814877000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ed017800000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004193acc98d79044e8ec1bc3ced832dc679e38ac8c6fe9b5befd1e5e44cb44edb0e365f1c5d6e3ca6590ed1a053f1841aede29e5b573f046387aff794520a0f22581b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 242373460199848577067005852); - } - - function testUSV4IntegrationOutputETH() 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); - - // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_out` - (bool success,) = tychoRouterAddr.call( - hex"d499aa8800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000006814878000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ed018800000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004190134d2d142caff6dbea417292a15685119bd676b2b73bad35fe39f720f7c3163f16d057327499019506b6f690a3916fd3375c579c9cb814113b1516187380531b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c0000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balanceAfter = ALICE.balance; - - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 1117254495486192350); - } - - function testSingleSwapWithWrapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // 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); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_wrap` - (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006813638900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebdd91000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f6ff7411a7ec76cb7dcafecf5e7f11121b1aa88af505635dc7faae6057e4f44e2859712f58331a14a1624f1e5edf2af80ddd2d90b5453d74df1b1fea10b9a2f91c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" - ); - - vm.stopPrank(); - - uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 2659881924818443699787); - } - - function testSingleSwapWithUnwrapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // 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); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_unwrap` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be00000000000000000000000000000000000000000000000000000000000006813615200000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebdb5a000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a7da748b04674485a5da185055affefc85b6d8fe412accce55b6f67842116f0f7f7130de5d74c68c20e1cedcdf93b8741b9171de2e6a3f2567887382a0712e3f1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000" - ); - - vm.stopPrank(); - - uint256 balanceAfter = ALICE.balance; - - assertTrue(success, "Call Failed"); - assertEq(balanceAfter - balanceBefore, 1120007305574805922); - } - - function testEkuboIntegration() public { - // Test needs to be run on block 22082754 or later - // notice that the addresses for the tycho router and the executors are different because we are redeploying - vm.rollFork(22082754); - tychoRouter = deployRouter(); - address[] memory executors = deployExecutors(); - vm.startPrank(EXECUTOR_SETTER); - tychoRouter.setExecutors(executors); - vm.stopPrank(); - - // TEMPORARY while the Ekubo executor address is hardcoded in TychoRouter - // This allows us to change the code at that address to be the testing executor code - vm.etch( - 0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D, - 0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7.code - ); - - deal(ALICE, 1 ether); - uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE); - - vm.startPrank(ALICE); - // Encoded solution generated using `test_split_encoding_strategy_ekubo` - (bool success,) = address(tychoRouter).call{value: 1 ether}( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000003d7ebc40af7092e3f1c81f2e996cba5cae2090d7a4ad4f68d0b91cfd19687c881e50f3a00242828c0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" - ); - - 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 testSplitSwapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // 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); - // Encoded solution generated using `test_split_swap_strategy_encoder_complex` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000681363a300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebddab0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000415b7ff43991de10c4c3a0372653891d27eb305ce04228bfe46a7d84a0978063fc4cb05183f19b83511bcb689b002d4f8e170f1d3cd77cf18c638229ccb67e0cac1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160005600028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005602030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005601030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d0139501" - ); - - 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 testSwapAmountInNotFullySpent() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Has invalid data as input! There is only one swap with 60% of the input amount - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSwap( - uint8(0), - uint8(1), - (0xffffff * 60) / 100, // 60% - address(usv2Executor), - protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - vm.expectRevert( - abi.encodeWithSelector( - TychoRouter__AmountInDiffersFromConsumed.selector, - 1000000000000000000, - 600000000000000000 - ) - ); - - tychoRouter.swapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - 1, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - vm.stopPrank(); - } - - function testSwapSingleUSV4Callback() public { - uint256 amountIn = 100 ether; - deal(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, address(usv4Executor), pools - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - - assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852); - } - - function testSwapSingleUSV4CallbackPermit2() public { - vm.startPrank(ALICE); - uint256 amountIn = 100 ether; - deal(USDE_ADDR, ALICE, amountIn); - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(USDE_ADDR, 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, address(usv4Executor), pools - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.swapPermit2( - amountIn, - USDE_ADDR, - USDT_ADDR, - 99943850, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99943852); - vm.stopPrank(); - } - - function testSwapMultipleUSV4Callback() 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, address(usv4Executor), pools - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - - assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); - } - - function testCyclicSequentialSwap() public { - // This test has start and end tokens that are the same - // The flow is: - // USDC -> WETH -> USDC using two pools - uint256 amountIn = 100 * 10 ** 6; - deal(USDC_ADDR, tychoRouterAddr, amountIn); - - bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap( - USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true - ); - - bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( - WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false - ); - - bytes[] memory swaps = new bytes[](2); - // USDC -> WETH - swaps[0] = encodeSwap( - uint8(0), - uint8(1), - uint24(0), - address(usv3Executor), - usdcWethV3Pool1ZeroOneData - ); - // WETH -> USDC - swaps[1] = encodeSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2OneZeroData - ); - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); - } - - function testSplitInputCyclicSwap() 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, tychoRouterAddr, amountIn); - - bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap( - USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true - ); - - bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap( - USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true - ); - - bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap( - WETH_ADDR, USDC_WETH_USV2, tychoRouterAddr, false - ); - - bytes[] memory swaps = new bytes[](3); - // USDC -> WETH (60% split) - swaps[0] = encodeSwap( - uint8(0), - uint8(1), - (0xffffff * 60) / 100, // 60% - address(usv3Executor), - usdcWethV3Pool1ZeroOneData - ); - // USDC -> WETH (40% remainder) - swaps[1] = encodeSwap( - uint8(0), - uint8(1), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2ZeroOneData - ); - // WETH -> USDC - swaps[2] = encodeSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv2Executor), - wethUsdcV2OneZeroData - ); - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171); - } - - function testSplitOutputCyclicSwap() 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 - ); - - bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap( - WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false - ); - - bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( - WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false - ); - - bytes[] memory swaps = new bytes[](3); - // USDC -> WETH - swaps[0] = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data - ); - // WETH -> USDC - swaps[1] = encodeSwap( - uint8(1), - uint8(0), - (0xffffff * 60) / 100, - address(usv3Executor), - usdcWethV3Pool1OneZeroData - ); - - // WETH -> USDC - swaps[2] = encodeSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2OneZeroData - ); - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99525908); - } - - function testCyclicSequentialSwapIntegration() public { - deal(USDC_ADDR, ALICE, 100 * 10 ** 6); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_cyclic_sequential_swap` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000681363d200000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebddda0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000418d58a54a3b8afc5d2e228ce6c5a1ab6b342cb5bfd9a00d57b869a4703ca2bb084d10d21f6842be9652a9ff2392673fbdcb961439ccc962de09f6bc64e5e665fe1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000" - ); - - assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99889294); - - vm.stopPrank(); - } - - function testSplitInputCyclicSwapIntegration() public { - deal(USDC_ADDR, ALICE, 100 * 10 ** 6); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_input_cyclic_swap` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000681363ee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebddf6000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041358738c580b15c5aeb2cd79615e7405569255d599e45d2d537805c4d403a8ce4198cdde7c328a881afeb2f5dc721c5d13dfae03ded6e8e958a96e303e7fa07e91b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136006d00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d801005601000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000" - ); - - assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99574171); - - 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); - // Encoded solution generated using `test_split_output_cyclic_swap` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005eea514000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006813641000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebde18000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041261a267c7d90a230d7f6d0917652953ef5cdaaabc80234a0c3d39ca20687f5af0b56421d0b0bec01d5ba66dd435d7cd63e95abcea114aa9fef6fe9d77589c12e1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d0139501006d01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564000006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80000000000000000000000" - ); - - assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99525908); - - vm.stopPrank(); - } - - // Base Network Tests - // Make sure to set the RPC_URL to base network - function testSwapSingleBase() 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 - ); - - bytes memory swap = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606); - } - - function testCurveIntegration() public { - deal(UWU_ADDR, ALICE, 1 ether); - - vm.startPrank(ALICE); - IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max); - // Encoded solution generated using `test_split_encoding_strategy_curve` - (bool success,) = tychoRouterAddr.call( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e71020100010000000000" - ); - - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 4691958787921); - - vm.stopPrank(); - } - - function testCurveIntegrationStETH() public { - deal(ALICE, 1 ether); - - vm.startPrank(ALICE); - // Encoded solution generated using `test_split_encoding_strategy_curve_st_eth` - (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe840000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f67022010001000000000000" - ); - - assertEq(IERC20(STETH_ADDR).balanceOf(ALICE), 1000754689941529590); - - vm.stopPrank(); + tychoRouter.exposedSplitSwap(amountIn, 2, swaps); } } diff --git a/foundry/test/TychoRouterProtocolIntegration.t.sol b/foundry/test/TychoRouterProtocolIntegration.t.sol new file mode 100644 index 0000000..08eb97b --- /dev/null +++ b/foundry/test/TychoRouterProtocolIntegration.t.sol @@ -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(); + } +} diff --git a/foundry/test/TychoRouterSequentialSwap.t.sol b/foundry/test/TychoRouterSequentialSwap.t.sol new file mode 100644 index 0000000..1d44076 --- /dev/null +++ b/foundry/test/TychoRouterSequentialSwap.t.sol @@ -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); + } +} diff --git a/foundry/test/TychoRouterSingleSwap.t.sol b/foundry/test/TychoRouterSingleSwap.t.sol new file mode 100644 index 0000000..bf49fbd --- /dev/null +++ b/foundry/test/TychoRouterSingleSwap.t.sol @@ -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); + } +} diff --git a/foundry/test/TychoRouterSplitSwap.t.sol b/foundry/test/TychoRouterSplitSwap.t.sol new file mode 100644 index 0000000..417b350 --- /dev/null +++ b/foundry/test/TychoRouterSplitSwap.t.sol @@ -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(); + } +} diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 025fd71..7757d49 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -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 ); } } diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt new file mode 100644 index 0000000..bf5c417 --- /dev/null +++ b/foundry/test/assets/calldata.txt @@ -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 diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index 9187187..1ad790e 100644 --- a/foundry/test/executors/BalancerV2Executor.t.sol +++ b/foundry/test/executors/BalancerV2Executor.t.sol @@ -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); diff --git a/foundry/test/executors/CurveExecutor.t.sol b/foundry/test/executors/CurveExecutor.t.sol index cd15079..629ce79 100644 --- a/foundry/test/executors/CurveExecutor.t.sol +++ b/foundry/test/executors/CurveExecutor.t.sol @@ -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 ); } diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index 0971b40..23996f9 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -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")); } } diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index a26a9b7..5f81427 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -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); diff --git a/foundry/test/executors/UniswapV3Executor.t.sol b/foundry/test/executors/UniswapV3Executor.t.sol index 01f5d1c..e9d95e6 100644 --- a/foundry/test/executors/UniswapV3Executor.t.sol +++ b/foundry/test/executors/UniswapV3Executor.t.sol @@ -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 ); } } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index e3b3cb6..8f2c292 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -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); } } diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol index b84bb69..c96465c 100644 --- a/foundry/test/executors/UniswapV4Utils.sol +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -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 ); } } diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index c0216fa..a1fcf00 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -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, #[arg(short, long)] router_address: Option, + #[arg(short, long)] + swapper_pk: Option, + #[arg(short, long)] + token_in_already_in_router: Option, } #[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> { 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); - - 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)? + let chain = cli.chain; + let encoder: Box = 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); + } + if let Some(swapper_pk) = cli.swapper_pk { + builder = builder.swapper_pk(swapper_pk); + } + 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::DirectExecution => builder.initialize_direct_execution()?, + 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> { // 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(()) diff --git a/src/encoding/evm/approvals/permit2.rs b/src/encoding/evm/approvals/permit2.rs index c1ccf2c..51b1f6a 100644 --- a/src/encoding/evm/approvals/permit2.rs +++ b/src/encoding/evm/approvals/permit2.rs @@ -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] diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index 8de957b..364a4ce 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -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}" ))), } } diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index feba1cc..37d8aff 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -13,7 +13,36 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str = pub static GROUPABLE_PROTOCOLS: LazyLock> = 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> = 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> = LazyLock::new(|| { + let mut set = HashSet::new(); + set.insert("uniswap_v3"); + set.insert("pancakeswap_v3"); + set.insert("uniswap_v4"); set.insert("ekubo_v2"); set }); diff --git a/src/encoding/evm/encoder_builder.rs b/src/encoding/evm/encoder_builder.rs deleted file mode 100644 index e20c148..0000000 --- a/src/encoding/evm/encoder_builder.rs +++ /dev/null @@ -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>, - chain: Option, - executors_file_path: Option, - router_address: Option, -} - -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) -> 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 { - 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 { - 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 { - 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 { - 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(), - )) - } - } -} diff --git a/src/encoding/evm/encoder_builders.rs b/src/encoding/evm/encoder_builders.rs new file mode 100644 index 0000000..ee412ac --- /dev/null +++ b/src/encoding/evm/encoder_builders.rs @@ -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, + chain: Option, + executors_file_path: Option, + router_address: Option, + token_in_already_in_router: Option, +} + +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, 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 = + 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, + executors_file_path: Option, +} + +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, 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(), + )) + } + } +} diff --git a/src/encoding/evm/strategy_encoder/group_swaps.rs b/src/encoding/evm/group_swaps.rs similarity index 90% rename from src/encoding/evm/strategy_encoder/group_swaps.rs rename to src/encoding/evm/group_swaps.rs index 0e4206c..5ec5fe6 100644 --- a/src/encoding/evm/strategy_encoder/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, } @@ -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, } diff --git a/src/encoding/evm/mod.rs b/src/encoding/evm/mod.rs index b86b4dd..344aaa1 100644 --- a/src/encoding/evm/mod.rs +++ b/src/encoding/evm/mod.rs @@ -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; diff --git a/src/encoding/evm/strategy_encoder/mod.rs b/src/encoding/evm/strategy_encoder/mod.rs index 14137b6..260436b 100644 --- a/src/encoding/evm/strategy_encoder/mod.rs +++ b/src/encoding/evm/strategy_encoder/mod.rs @@ -1,3 +1,4 @@ -mod group_swaps; pub mod strategy_encoders; mod strategy_validators; + +mod transfer_optimizations; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 0a4dcfc..34f2a47 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1,7 +1,4 @@ -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, -}; +use std::{collections::HashSet, str::FromStr}; use alloy_primitives::{aliases::U24, U256, U8}; use alloy_sol_types::SolValue; @@ -11,12 +8,15 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, - constants::DEFAULT_ROUTERS_JSON, - strategy_encoder::{group_swaps::group_swaps, strategy_validators::SplitSwapValidator}, + group_swaps::group_swaps, + strategy_encoder::{ + strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, + transfer_optimizations::TransferOptimization, + }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ biguint_to_u256, bytes_to_address, encode_input, get_min_amount_for_solution, - get_token_position, percentage_to_uint24, + get_token_position, percentage_to_uint24, ple_encode, }, }, models::{Chain, EncodingContext, NativeAction, Solution}, @@ -24,10 +24,438 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; -/// Encodes a solution using a specific strategy for execution on the EVM-compatible network. -pub trait EVMStrategyEncoder: StrategyEncoder { - /// Encodes information necessary for performing a single swap against a given executor for +/// Represents the encoder for a swap strategy which supports single swaps. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders +/// * `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 +/// * `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, + router_address: Bytes, + transfer_optimization: TransferOptimization, +} + +impl SingleSwapStrategyEncoder { + 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())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) + } else { + ( + None, + "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, + 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, + ), + }) + } + + /// Encodes information necessary for performing a single hop against a given executor for /// a protocol. + fn encode_swap_header(&self, executor_address: Bytes, protocol_data: Vec) -> Vec { + let mut encoded = Vec::new(); + encoded.extend(executor_address.to_vec()); + encoded.extend(protocol_data); + encoded + } +} + +impl StrategyEncoder for SingleSwapStrategyEncoder { + fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { + let grouped_swaps = group_swaps(solution.clone().swaps); + let number_of_groups = grouped_swaps.len(); + if number_of_groups != 1 { + return Err(EncodingError::InvalidInput(format!( + "Executor strategy only supports exactly one swap for non-groupable protocols. Found {number_of_groups}", + ))) + } + + let grouped_swap = grouped_swaps + .first() + .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; + + if grouped_swap.split != 0f64 { + return Err(EncodingError::InvalidInput( + "Splits not supported for single swaps.".to_string(), + )) + } + + let min_amount_out = get_min_amount_for_solution(solution.clone()); + + let (mut unwrap, mut wrap) = (false, false); + if let Some(action) = solution.native_action.clone() { + match action { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, + } + } + let protocol = grouped_swap.protocol_system.clone(); + let swap_encoder = self + .get_swap_encoder(&protocol) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {protocol}" + )) + })?; + + 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 protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; + grouped_protocol_data.extend(protocol_data); + } + + let swap_data = self.encode_swap_header( + Bytes::from_str(swap_encoder.executor_address()) + .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?, + grouped_protocol_data, + ); + + let method_calldata = if let Some(permit2) = self.permit2.clone() { + let (permit, signature) = permit2.get_permit( + &self.router_address, + &solution.sender, + &solution.given_token, + &solution.given_amount, + )?; + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + permit, + signature.as_bytes().to_vec(), + swap_data, + ) + .abi_encode() + } else { + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + swap_data, + ) + .abi_encode() + }; + + let contract_interaction = encode_input(&self.selector, method_calldata); + Ok((contract_interaction, self.router_address.clone())) + } + + fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { + self.swap_encoder_registry + .get_encoder(protocol_system) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Represents the encoder for a swap strategy which supports sequential swaps. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders +/// * `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 +/// * `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, + permit2: Option, + selector: String, + router_address: Bytes, + native_address: Bytes, + wrapped_address: Bytes, + sequential_swap_validator: SequentialSwapValidator, + transfer_optimization: TransferOptimization, +} + +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()) + } else { + ( + None, + "sequentialSwap(uint256,address,address,uint256,bool,bool,address,bytes)" + .to_string(), + ) + }; + let permit2_is_active = permit2.is_some(); + Ok(Self { + permit2, + selector, + swap_encoder_registry, + 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, + ), + }) + } + + /// Encodes information necessary for performing a single hop against a given executor for + /// a protocol. + fn encode_swap_header(&self, executor_address: Bytes, protocol_data: Vec) -> Vec { + let mut encoded = Vec::new(); + encoded.extend(executor_address.to_vec()); + encoded.extend(protocol_data); + encoded + } +} + +impl StrategyEncoder for SequentialSwapStrategyEncoder { + fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { + self.sequential_swap_validator + .validate_solution_min_amounts(&solution)?; + self.sequential_swap_validator + .validate_swap_path( + &solution.swaps, + &solution.given_token, + &solution.checked_token, + &solution.native_action, + &self.native_address, + &self.wrapped_address, + )?; + + let min_amount_out = get_min_amount_for_solution(solution.clone()); + let grouped_swaps = group_swaps(solution.swaps); + + let (mut unwrap, mut wrap) = (false, false); + if let Some(action) = solution.native_action.clone() { + match action { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, + } + } + + let mut swaps = vec![]; + 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 + .get_swap_encoder(&protocol) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {protocol}", + )) + })?; + + let in_between_swap_optimization_allowed = next_in_between_swap_optimization_allowed; + let next_swap = grouped_swaps.get(i + 1); + 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 protocol_data = + swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; + grouped_protocol_data.extend(protocol_data); + } + + let swap_data = self.encode_swap_header( + Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { + EncodingError::FatalError("Invalid executor address".to_string()) + })?, + grouped_protocol_data, + ); + swaps.push(swap_data); + } + + let encoded_swaps = ple_encode(swaps); + let method_calldata = if let Some(permit2) = self.permit2.clone() { + let (permit, signature) = permit2.get_permit( + &self.router_address, + &solution.sender, + &solution.given_token, + &solution.given_amount, + )?; + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + permit, + signature.as_bytes().to_vec(), + encoded_swaps, + ) + .abi_encode() + } else { + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + encoded_swaps, + ) + .abi_encode() + }; + + let contract_interaction = encode_input(&self.selector, method_calldata); + Ok((contract_interaction, self.router_address.clone())) + } + + fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { + self.swap_encoder_registry + .get_encoder(protocol_system) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Represents the encoder for a swap strategy which supports split swaps. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders +/// * `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 +/// * `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, + permit2: Option, + selector: String, + native_address: Bytes, + wrapped_address: Bytes, + split_swap_validator: SplitSwapValidator, + router_address: Bytes, + transfer_optimization: TransferOptimization, +} + +impl SplitSwapStrategyEncoder { + 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())?), "splitSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) + } else { + ( + None, + "splitSwap(uint256,address,address,uint256,bool,bool,uint256,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()?, + split_swap_validator: SplitSwapValidator, + 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, + ), + }) + } + + /// Encodes information necessary for performing a single hop against a given executor for + /// a protocol as part of a split swap solution. fn encode_swap_header( &self, token_in: U8, @@ -44,90 +472,8 @@ pub trait EVMStrategyEncoder: StrategyEncoder { encoded.extend(protocol_data); encoded } - - /// 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. - fn ple_encode(&self, action_data_array: Vec>) -> Vec { - let mut encoded_action_data: Vec = 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 - } } -/// Represents the encoder for a swap strategy which supports single, sequential and split swaps. -/// -/// # Fields -/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders -/// * `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 -/// * `split_swap_validator`: SplitSwapValidator, responsible for checking validity of split swap -/// solutions -/// * `router_address`: Address of the router to be used to execute swaps -#[derive(Clone)] -pub struct SplitSwapStrategyEncoder { - swap_encoder_registry: SwapEncoderRegistry, - permit2: Option, - selector: String, - native_address: Bytes, - wrapped_address: Bytes, - split_swap_validator: SplitSwapValidator, - router_address: Bytes, -} - -impl SplitSwapStrategyEncoder { - pub fn new( - blockchain: tycho_common::models::Chain, - swap_encoder_registry: SwapEncoderRegistry, - swapper_pk: Option, - router_address: Option, - ) -> Result { - let chain = Chain::from(blockchain); - let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { - (Some(Permit2::new(swapper_pk, chain.clone())?), "swapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) - } else { - ( - None, - "swap(uint256,address,address,uint256,bool,bool,uint256,address,bytes)".to_string(), - ) - }; - - let tycho_router_address; - if let Some(address) = router_address { - tycho_router_address = address; - } else { - let default_routers: HashMap = - 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(); - } - - Ok(Self { - permit2, - selector, - swap_encoder_registry, - native_address: chain.native_token()?, - wrapped_address: chain.wrapped_token()?, - split_swap_validator: SplitSwapValidator, - router_address: tycho_router_address, - }) - } -} -impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} - impl StrategyEncoder for SplitSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.split_swap_validator @@ -158,7 +504,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 @@ -193,32 +539,42 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { let mut swaps = vec![]; for grouped_swap in grouped_swaps.iter() { + let protocol = grouped_swap.protocol_system.clone(); let swap_encoder = self - .get_swap_encoder(&grouped_swap.protocol_system) + .get_swap_encoder(&protocol) .ok_or_else(|| { EncodingError::InvalidInput(format!( - "Swap encoder not found for protocol: {}", - grouped_swap.protocol_system + "Swap encoder not found for protocol: {protocol}", )) })?; + 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 encoding_context = EncodingContext { - receiver: self.router_address.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(), - }; 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()) @@ -228,7 +584,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { swaps.push(swap_data); } - let encoded_swaps = self.ple_encode(swaps); + let encoded_swaps = ple_encode(swaps); let tokens_len = if solution.given_token == solution.checked_token { tokens.len() - 1 } else { @@ -284,77 +640,6 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } } -/// This strategy encoder is used for solutions that are sent directly to the executor, bypassing -/// the router. Only one solution with one swap is supported. -/// -/// # Fields -/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders -#[derive(Clone)] -pub struct ExecutorStrategyEncoder { - swap_encoder_registry: SwapEncoderRegistry, -} - -impl ExecutorStrategyEncoder { - pub fn new(swap_encoder_registry: SwapEncoderRegistry) -> Self { - Self { swap_encoder_registry } - } -} -impl EVMStrategyEncoder for ExecutorStrategyEncoder {} -impl StrategyEncoder for ExecutorStrategyEncoder { - fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - let grouped_swaps = group_swaps(solution.clone().swaps); - let number_of_groups = grouped_swaps.len(); - if number_of_groups > 1 { - return Err(EncodingError::InvalidInput(format!( - "Executor strategy only supports one swap for non-groupable protocols. Found {}", - number_of_groups - ))) - } - - let grouped_swap = grouped_swaps - .first() - .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; - - let receiver = solution.receiver; - - let swap_encoder = self - .get_swap_encoder(&grouped_swap.protocol_system) - .ok_or_else(|| { - EncodingError::InvalidInput(format!( - "Swap encoder not found for protocol: {}", - grouped_swap.protocol_system - )) - })?; - - let mut grouped_protocol_data: Vec = vec![]; - for swap in grouped_swap.swaps.iter() { - let encoding_context = EncodingContext { - 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(), - }; - let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; - grouped_protocol_data.extend(protocol_data); - } - - let executor_address = Bytes::from_str(swap_encoder.executor_address()) - .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; - - Ok((grouped_protocol_data, executor_address)) - } - - fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { - self.swap_encoder_registry - .get_encoder(protocol_system) - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - #[cfg(test)] mod tests { use std::{collections::HashMap, str::FromStr}; @@ -364,15 +649,15 @@ mod tests { use num_bigint::{BigInt, BigUint}; use rstest::rstest; use tycho_common::{ - models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, + models::{protocol::ProtocolComponent, Chain as TychoCommonChain}, Bytes, }; use super::*; - use crate::encoding::models::Swap; + use crate::encoding::{evm::utils::write_calldata_to_file, models::Swap}; - fn eth_chain() -> TychoCoreChain { - TychoCoreChain::Ethereum + fn eth_chain() -> Chain { + TychoCommonChain::Ethereum.into() } fn eth() -> Bytes { @@ -389,1512 +674,2046 @@ mod tests { .unwrap() } - #[test] - fn test_executor_strategy_encode() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let token_in = weth(); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from(1000000000000000000u64), - expected_amount: Some(BigUint::from(1000000000000000000u64)), - checked_token: token_out, - checked_amount: None, - sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), - swaps: vec![swap], - slippage: None, - native_action: None, - }; - - let (protocol_data, executor_address) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_protocol_data = encode(&protocol_data); - assert_eq!( - executor_address, - Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() - ); - assert_eq!( - hex_protocol_data, - String::from(concat!( - // in token - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // component id - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", - // receiver - "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - // zero for one - "00", - )) - ); - } - - #[test] - fn test_executor_strategy_encode_too_many_swaps() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let token_in = weth(); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from(1000000000000000000u64), - expected_amount: Some(BigUint::from(1000000000000000000u64)), - checked_token: token_out, - checked_amount: None, - sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), - receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), - swaps: vec![swap.clone(), swap], - slippage: None, - native_action: None, - }; - - let result = encoder.encode_strategy(solution); - assert!(result.is_err()); - } - - #[test] - fn test_executor_strategy_encode_grouped_swaps() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let eth = eth(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); - - // Fee and tick spacing information for this test is obtained by querying the - // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e - // Using the poolKeys function with the first 25 bytes of the pool id - let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); - let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); - let mut static_attributes_usdc_eth: HashMap = HashMap::new(); - static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); - static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); - - let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); - let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); - let mut static_attributes_eth_pepe: HashMap = HashMap::new(); - static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); - static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); - - let swap_usdc_eth = Swap { - component: ProtocolComponent { - id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_usdc_eth, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: eth.clone(), - split: 0f64, - }; - - let swap_eth_pepe = Swap { - component: ProtocolComponent { - id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_eth_pepe, - ..Default::default() - }, - token_in: eth.clone(), - token_out: pepe.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: usdc, - given_amount: BigUint::from_str("1000_000000").unwrap(), - checked_token: pepe, - expected_amount: Some(BigUint::from_str("105_152_000000000000000000").unwrap()), - checked_amount: None, - slippage: None, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_usdc_eth, swap_eth_pepe], - ..Default::default() - }; - - let (protocol_data, executor_address) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_protocol_data = encode(&protocol_data); - assert_eq!( - executor_address, - Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() - ); - assert_eq!( - hex_protocol_data, - String::from(concat!( - // group in token - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // group out token - "6982508145454ce325ddbe47a25d4ec3d2311933", - // zero for one - "00", - // executor address - "f62849f9a0b5bf2913b396098f7c7019b51a820a", - // first pool intermediary token (ETH) - "0000000000000000000000000000000000000000", - // fee - "000bb8", - // tick spacing - "00003c", - // second pool intermediary token (PEPE) - "6982508145454ce325ddbe47a25d4ec3d2311933", - // fee - "0061a8", - // tick spacing - "0001f4" - )) - ); - } - - #[rstest] - #[case::with_check_no_slippage( - None, - None, - Some(BigUint::from_str("2659881924818443699787").unwrap()), - U256::from_str("2659881924818443699787").unwrap(), - )] - #[case::no_check_with_slippage( - Some(BigUint::from_str("3_000_000000000000000000").unwrap()), - Some(0.01f64), - None, - U256::from_str("2_970_000000000000000000").unwrap(), - )] - #[case::with_check_and_slippage( - Some(BigUint::from_str("3_000_000000000000000000").unwrap()), - Some(0.01f64), - Some(BigUint::from_str("2_999_000000000000000000").unwrap()), - U256::from_str("2_999_000000000000000000").unwrap(), - )] - fn test_split_swap_strategy_encoder_simple_route( - #[case] expected_amount: Option, - #[case] slippage: Option, - #[case] checked_amount: Option, - #[case] expected_min_amount: U256, - ) { - // Performs a single swap from WETH to DAI on a USV2 pool, with no grouping optimizations. - - // Set up a mock private key for signing - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); - let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").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 = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .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 = [ - "d499aa88", // Function selector - "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out - "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out - &expected_min_amount_encoded, // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap - "0000000000000000000000000000000000000000000000000000000000000002", // tokens length - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - ] - .join(""); - - // after this there is the permit and because of the deadlines (that depend on block time) - // it's hard to assert - // "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - // "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in - // "0000000000000000000000000000000000000000000000000000000067c205fe", // expiration - // "0000000000000000000000000000000000000000000000000000000000000000", // nonce - // "0000000000000000000000002c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4", // spender - // "00000000000000000000000000000000000000000000000000000000679a8006", // deadline - // offset of signature (from start of call data to beginning of length indication) - // "0000000000000000000000000000000000000000000000000000000000000200", - // offset of ple encoded swaps (from start of call data to beginning of length indication) - // "0000000000000000000000000000000000000000000000000000000000000280", - // length of signature without padding - // "0000000000000000000000000000000000000000000000000000000000000041", - // signature + padding - // "a031b63a01ef5d25975663e5d6c420ef498e3a5968b593cdf846c6729a788186", - // "1ddaf79c51453cd501d321ee541d13593e3a266be44103eefdf6e76a032d2870", - // "1b00000000000000000000000000000000000000000000000000000000000000" - - let expected_swaps = String::from(concat!( - // length of ple encoded swaps without padding - "0000000000000000000000000000000000000000000000000000000000000058", - // ple encoded swaps - "0056", - // Swap header - "00", // token in index - "01", // token out index - "000000", // split - // Swap data - "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver - "00", // zero2one - "00", // exact out - "00000000000000", // padding - )); - let hex_calldata = encode(&calldata); - - assert_eq!(hex_calldata[..520], expected_input); - assert_eq!(hex_calldata[1288..], expected_swaps); - } - - #[test] - fn test_split_swap_strategy_encoder_simple_route_wrap() { - // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - - // Set up a mock private key for signing - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai.clone(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - let solution = Solution { - exact_out: false, - given_token: eth(), - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: dai, - expected_amount: None, - checked_amount: Some(BigUint::from_str("2659881924818443699787").unwrap()), - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_swap_strategy_encoder_simple_route_unwrap() { - // Performs a single swap from DAI to WETH on a USV2 pool, unwrapping ETH at the end - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - - // Set up a mock private key for signing - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai.clone(), - token_out: weth(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - let solution = Solution { - exact_out: false, - given_token: dai, - given_amount: BigUint::from_str("3_000_000000000000000000").unwrap(), - checked_token: eth(), - expected_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), - checked_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_swap_strategy_encoder_complex_route() { - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - // - // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools - // - // ┌──(USV2)──> WBTC ───(USV2)──> USDC - // WETH ─┤ - // └──(USV2)──> DAI ───(USV2)──> USDC - // - - // Set up a mock private key for signing - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let weth = weth(); - let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - - let swap_weth_dai = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5f64, - }; - let swap_weth_wbtc = Swap { - component: ProtocolComponent { - id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: wbtc.clone(), - // This represents the remaining 50%, but to avoid any rounding errors we set this to - // 0 to signify "the remainder of the WETH value". It should still be very close to 50% - split: 0f64, - }; - let swap_dai_usdc = Swap { - component: ProtocolComponent { - id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { - id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - let solution = Solution { - exact_out: false, - given_token: weth, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: usdc, - expected_amount: None, - checked_amount: Some(BigUint::from_str("26173932").unwrap()), - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let _hex_calldata = encode(&calldata); - println!("{}", _hex_calldata); - } - - #[test] - fn test_split_encoding_strategy_usv4() { - // Performs a sequential swap from USDC to PEPE though ETH using two consecutive USV4 pools - // - // USDC ──(USV4)──> ETH ───(USV4)──> PEPE - // - - // Set up a mock private key for signing (Alice's pk in our router tests) - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let eth = eth(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); - - // Fee and tick spacing information for this test is obtained by querying the - // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e - // Using the poolKeys function with the first 25 bytes of the pool id - let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); - let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); - let mut static_attributes_usdc_eth: HashMap = HashMap::new(); - static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); - static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); - - let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); - let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); - let mut static_attributes_eth_pepe: HashMap = HashMap::new(); - static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); - static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); - - let swap_usdc_eth = Swap { - component: ProtocolComponent { - id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_usdc_eth, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: eth.clone(), - split: 0f64, - }; - - let swap_eth_pepe = Swap { - component: ProtocolComponent { - id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_eth_pepe, - ..Default::default() - }, - token_in: eth.clone(), - token_out: pepe.clone(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - let solution = Solution { - exact_out: false, - given_token: usdc, - given_amount: BigUint::from_str("1000_000000").unwrap(), - checked_token: pepe, - expected_amount: None, - checked_amount: Some(BigUint::from_str("97191013220606467325121599").unwrap()), - slippage: None, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_usdc_eth, swap_eth_pepe], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let expected_input = [ - "d499aa88", // Function selector - "000000000000000000000000000000000000000000000000000000003b9aca00", // amount in - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in - "0000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933", // token out - "0000000000000000000000000000000000000000005064ff624d54346285543f", // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap - // tokens length (not including intermediary tokens of USV4-optimized swaps) - "0000000000000000000000000000000000000000000000000000000000000002", - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - ] - .join(""); - - // after this there is the permit and because of the deadlines (that depend on block time) - // it's hard to assert - // "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - // "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in - // "0000000000000000000000000000000000000000000000000000000067c205fe", // expiration - // "0000000000000000000000000000000000000000000000000000000000000000", // nonce - // "0000000000000000000000002c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4", // spender - // "00000000000000000000000000000000000000000000000000000000679a8006", // deadline - // offset of signature (from start of call data to beginning of length indication) - // "0000000000000000000000000000000000000000000000000000000000000200", - // offset of ple encoded swaps (from start of call data to beginning of length indication) - // "0000000000000000000000000000000000000000000000000000000000000280", - // length of signature without padding - // "0000000000000000000000000000000000000000000000000000000000000041", - // signature + padding - // "a031b63a01ef5d25975663e5d6c420ef498e3a5968b593cdf846c6729a788186", - // "1ddaf79c51453cd501d321ee541d13593e3a266be44103eefdf6e76a032d2870", - // "1b00000000000000000000000000000000000000000000000000000000000000" - - let expected_swaps = String::from(concat!( - // length of ple encoded swaps without padding - "000000000000000000000000000000000000000000000000000000000000008c", - // ple encoded swaps - "008a", // Swap length - "00", // token in index - "01", // token out index - "000000", // split - // Swap data header - "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address - // Protocol data - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // group token in - "6982508145454ce325ddbe47a25d4ec3d2311933", // group token in - "00", // zero2one - "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address - // First pool params - "0000000000000000000000000000000000000000", // intermediary token (ETH) - "000bb8", // fee - "00003c", // tick spacing - // Second pool params - "6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE) - "0061a8", // fee - "0001f4", // tick spacing - "0000000000000000000000000000000000000000" // padding - )); - - let hex_calldata = encode(&calldata); - - assert_eq!(hex_calldata[..520], expected_input); - assert_eq!(hex_calldata[1288..], expected_swaps); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_encoding_strategy_ekubo() { - // ETH ──(EKUBO)──> USDC - - let token_in = Bytes::from(Address::ZERO.as_slice()); - let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - - let static_attributes = HashMap::from([ - ("fee".to_string(), Bytes::from(0_u64)), - ("tick_spacing".to_string(), Bytes::from(0_u32)), - ("extension".to_string(), Bytes::from("0x51d02a5948496a67827242eabc5725531342527c")), /* Oracle */ - ]); - - let component = ProtocolComponent { - // All Ekubo swaps go through the core contract - not necessary to specify pool id - // for test - protocol_system: "ekubo_v2".to_string(), - static_attributes, - ..Default::default() - }; - - let swap = Swap { - component, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, + mod single { + use super::*; + #[rstest] + #[case::with_check_no_slippage( None, - Some(Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap()), - ) - .unwrap(); - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: token_out, - expected_amount: None, - checked_amount: Some(BigUint::from_str("1").unwrap()), - slippage: None, - // Alice - 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 hex_calldata = encode(&calldata); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_swap_strategy_encoder_simple_route_no_permit2() { - // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping - // optimizations. - - 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 = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, None, - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .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 = [ - "0a83cb08", // Function selector - "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out - "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out - &expected_min_amount_encoded, // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap - "0000000000000000000000000000000000000000000000000000000000000002", // tokens length - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - "0000000000000000000000000000000000000000000000000000000000000120", // offset of ple encoded swaps - "0000000000000000000000000000000000000000000000000000000000000058", // length of ple encoded swaps without padding - "0056", // ple encoded swaps - // Swap header - "00", // token in index - "01", // token out index - "000000", // split - // Swap data - "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver - "00", // zero2one - "00", // exact out - "00000000000000", // padding - ] - .join(""); - - let hex_calldata = encode(&calldata); - - assert_eq!(hex_calldata, expected_input); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_encoding_strategy_usv4_eth_in() { - // Performs a single swap from ETH to PEPE using a USV4 pool - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - // - // ETH ───(USV4)──> PEPE - // - // Set up a mock private key for signing (Alice's pk in our router tests) - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let eth = eth(); - let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); - - let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); - let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); - let mut static_attributes_eth_pepe: HashMap = HashMap::new(); - static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); - static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); - - let swap_eth_pepe = Swap { - component: ProtocolComponent { - id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_eth_pepe, - ..Default::default() - }, - token_in: eth.clone(), - token_out: pepe.clone(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - - let solution = Solution { - exact_out: false, - given_token: eth, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: pepe, - expected_amount: None, - checked_amount: Some(BigUint::from_str("242373460199848577067005852").unwrap()), - slippage: None, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_eth_pepe], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_calldata = encode(&calldata); - - println!("{}", hex_calldata); - } - #[test] - fn test_split_encoding_strategy_usv4_eth_out() { - // Performs a single swap from USDC to ETH using a USV4 pool - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - // - // USDC ───(USV4)──> ETH - // - // Set up a mock private key for signing (Alice's pk in our router tests) - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let eth = eth(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - - // Fee and tick spacing information for this test is obtained by querying the - // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e - // Using the poolKeys function with the first 25 bytes of the pool id - let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); - let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); - let mut static_attributes_usdc_eth: HashMap = HashMap::new(); - static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); - static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); - - let swap_usdc_eth = Swap { - component: ProtocolComponent { - id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_usdc_eth, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: eth.clone(), - split: 0f64, - }; - - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - - let solution = Solution { - exact_out: false, - given_token: usdc, - given_amount: BigUint::from_str("3000_000000").unwrap(), - checked_token: eth, - expected_amount: None, - checked_amount: Some(BigUint::from_str("1117254495486192350").unwrap()), - slippage: None, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_usdc_eth], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); - } - - #[test] - fn test_cyclic_sequential_swap() { - // This test has start and end tokens that are the same - // The flow is: - // USDC -> WETH -> USDC using two pools - - // Set up a mock private key for signing (Alice's pk in our router tests) - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - - // Create two Uniswap V3 pools for the cyclic swap - // USDC -> WETH (Pool 1) - let swap_usdc_weth = Swap { - component: ProtocolComponent { - id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 - * Pool 1 */ - protocol_system: "uniswap_v3".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(500).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0f64, - }; - - // WETH -> USDC (Pool 2) - let swap_weth_usdc = Swap { - component: ProtocolComponent { - id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 - * Pool 2 */ - protocol_system: "uniswap_v3".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(3000).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0f64, - }; - - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - - let solution = Solution { - exact_out: false, - given_token: usdc.clone(), - given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) - checked_token: usdc.clone(), - expected_amount: None, - checked_amount: Some(BigUint::from_str("99889294").unwrap()), /* Expected output from - * test */ - slippage: None, - swaps: vec![swap_usdc_weth, swap_weth_usdc], - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_calldata = hex::encode(&calldata); - let expected_input = [ - "d499aa88", // selector - "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token - "0000000000000000000000000000000000000000000000000000000005f4308e", // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap action - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap action - "0000000000000000000000000000000000000000000000000000000000000002", // tokens length - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - ] - .join(""); - - let expected_swaps = [ - "00000000000000000000000000000000000000000000000000000000000000de", // length of ple encoded swaps without padding - "006d", // ple encoded swaps - "00", // token in index - "01", // token out index - "000000", // split - "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out - "0001f4", // pool fee - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id - "01", // zero2one - "006d", // ple encoded swaps - "01", // token in index - "00000000", // split - "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out - "000bb8", // pool fee - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id - "000000", // zero2one - ] - .join(""); - - assert_eq!(hex_calldata[..520], expected_input); - assert_eq!(hex_calldata[1288..], expected_swaps); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_input_cyclic_swap() { - // 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 ─┘ - - // Set up a mock private key for signing (Alice's pk in our router tests) - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - - // USDC -> WETH (Pool 1) - 60% of input - let swap_usdc_weth_pool1 = Swap { - component: ProtocolComponent { - id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 - * Pool 1 */ - protocol_system: "uniswap_v3".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(500).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0.6f64, // 60% of input - }; - - // USDC -> WETH (Pool 2) - 40% of input (remaining) - let swap_usdc_weth_pool2 = Swap { - component: ProtocolComponent { - id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 - * Pool 2 */ - protocol_system: "uniswap_v3".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(3000).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0f64, // Remaining 40% - }; - - // WETH -> USDC (Pool 2) - let swap_weth_usdc_pool2 = Swap { - component: ProtocolComponent { - id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2 - * Pool 2 */ - protocol_system: "uniswap_v2".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(3000).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.0f64, - }; - - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key.clone()), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - - let solution = Solution { - exact_out: false, - given_token: usdc.clone(), - given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) - checked_token: usdc.clone(), - expected_amount: None, - checked_amount: Some(BigUint::from_str("99574171").unwrap()), /* Expected output from - * test */ - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - slippage: None, - swaps: vec![swap_usdc_weth_pool1, swap_usdc_weth_pool2, swap_weth_usdc_pool2], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let hex_calldata = hex::encode(&calldata); - let expected_input = [ - "d499aa88", // selector - "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token - "0000000000000000000000000000000000000000000000000000000005ef619b", // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap action - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap action - "0000000000000000000000000000000000000000000000000000000000000002", // tokens length - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - ] - .join(""); - let expected_swaps = [ - "0000000000000000000000000000000000000000000000000000000000000136", // length of ple encoded swaps without padding - "006d", // ple encoded swaps - "00", // token in index - "01", // token out index - "999999", // split - "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out - "0001f4", // pool fee - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id - "01", // zero2one - "006d", // ple encoded swaps - "00", // token in index - "01", // token out index - "000000", // split - "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out - "000bb8", // pool fee - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id - "01", // zero2one - "0056", // ple encoded swaps - "01", // token in index - "00", // token out index - "000000", // split - "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address, - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id, - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "00", // zero2one - "00000000000000000000" // padding - ] - .join(""); - assert_eq!(hex_calldata[..520], expected_input); - assert_eq!(hex_calldata[1288..], expected_swaps); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_output_cyclic_swap() { - // This test has start and end tokens that are the same - // The flow is: - // ┌─── (USV3, 60% split) ───┐ - // │ │ - // USDC ──(USV2) ── WETH──| ├─> USDC - // │ │ - // └─── (USV3, 40% split) ───┘ - - // Set up a mock private key for signing (Alice's pk in our router tests) - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - - let swap_usdc_weth_v2 = Swap { - component: ProtocolComponent { - id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2 - protocol_system: "uniswap_v2".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(500).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0.0f64, - }; - - let swap_weth_usdc_v3_pool1 = Swap { - component: ProtocolComponent { - id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 - * Pool 1 */ - protocol_system: "uniswap_v3".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(500).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.6f64, - }; - - let swap_weth_usdc_v3_pool2 = Swap { - component: ProtocolComponent { - id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 - * Pool 2 */ - protocol_system: "uniswap_v3".to_string(), - static_attributes: { - let mut attrs = HashMap::new(); - attrs.insert( - "fee".to_string(), - Bytes::from(BigInt::from(3000).to_signed_bytes_be()), - ); - attrs - }, - ..Default::default() - }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.0f64, - }; - - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key.clone()), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), - ) - .unwrap(); - - let solution = Solution { - exact_out: false, - given_token: usdc.clone(), - given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) - checked_token: usdc.clone(), - expected_amount: None, - checked_amount: Some(BigUint::from_str("99525908").unwrap()), /* Expected output from - * test */ - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - slippage: None, - swaps: vec![swap_usdc_weth_v2, swap_weth_usdc_v3_pool1, swap_weth_usdc_v3_pool2], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - - let hex_calldata = hex::encode(&calldata); - let expected_input = [ - "d499aa88", // selector - "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token - "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token - "0000000000000000000000000000000000000000000000000000000005eea514", // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap action - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap action - "0000000000000000000000000000000000000000000000000000000000000002", // tokens length - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - ] - .join(""); - - let expected_swaps = [ - "0000000000000000000000000000000000000000000000000000000000000136", // length of ple encoded swaps without padding - "0056", // ple encoded swaps - "00", // token in index - "01", // token out index - "000000", // split - "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in - "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "01", // zero2one - "006d", // ple encoded swaps - "01", // token in index - "00", // token out index - "999999", // split - "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out - "0001f4", // pool fee - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id - "00", // zero2one - "006d", // ple encoded swaps - "01", // token in index - "00", // token out index - "000000", // split - "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out - "000bb8", // pool fee - "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address - "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id - "00", // zero2one - "00000000000000000000" // padding - ] - .join(""); - - assert_eq!(hex_calldata[..520], expected_input); - assert_eq!(hex_calldata[1288..], expected_swaps); - println!("{}", hex_calldata); - } - - #[test] - fn test_split_encoding_strategy_curve() { - // UWU ──(curve 2 crypto pool)──> WETH - - let token_in = Bytes::from("0x55C08ca52497e2f1534B59E2917BF524D4765257"); // UWU - let token_out = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // USDC - - let static_attributes = HashMap::from([( - "factory".to_string(), - Bytes::from( - "0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f" - .as_bytes() - .to_vec(), - ), - )]); - - let component = ProtocolComponent { - id: String::from("0x77146B0a1d08B6844376dF6d9da99bA7F1b19e71"), - protocol_system: String::from("vm:curve"), - static_attributes, - ..Default::default() - }; - - let swap = Swap { - component, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, + Some(BigUint::from_str("2659881924818443699787").unwrap()), + U256::from_str("2659881924818443699787").unwrap(), + )] + #[case::no_check_with_slippage( + Some(BigUint::from_str("2_000_000000000000000000").unwrap()), + Some(0.01f64), None, - Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), - ) - .unwrap(); + U256::from_str("1_980_000000000000000000").unwrap(), + )] + #[case::with_check_and_slippage( + Some(BigUint::from_str("2_000_000000000000000000").unwrap()), + Some(0.01f64), + Some(BigUint::from_str("1_999_000000000000000000").unwrap()), + U256::from_str("1_999_000000000000000000").unwrap(), + )] + fn test_single_swap_strategy_encoder( + #[case] expected_amount: Option, + #[case] slippage: Option, + #[case] checked_amount: Option, + #[case] expected_min_amount: U256, + ) { + // Performs a single swap from WETH to DAI on a USV2 pool, with no grouping + // optimizations. - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: token_out, - expected_amount: None, - checked_amount: Some(BigUint::from_str("1").unwrap()), - slippage: None, - // Alice - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap], - ..Default::default() - }; + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - let (calldata, _) = encoder - .encode_strategy(solution) + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").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, + Some(private_key), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount: expected_amount.clone(), + slippage, + checked_amount: checked_amount.clone(), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + ..Default::default() + }; - let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "30ace1b1", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + + // after this there is the permit and because of the deadlines (that depend on block + // time) it's hard to assert + + let expected_swap = String::from(concat!( + // length of encoded swap without padding + "0000000000000000000000000000000000000000000000000000000000000052", + // Swap data + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "00", // zero2one + "02", // transfer type + "0000000000000000000000000000", // padding + )); + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata[..456], expected_input); + assert_eq!(hex_calldata[1224..], expected_swap); + if expected_amount.is_some() & slippage.is_some() & checked_amount.is_none() { + // only write to file for 1 test case + write_calldata_to_file( + "test_single_swap_strategy_encoder", + &hex_calldata.to_string(), + ); + } + } + + #[test] + fn test_single_swap_strategy_encoder_no_permit2() { + // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no + // grouping optimizations. + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let expected_amount = Some(BigUint::from_str("1_650_000000000000000000").unwrap()); + let slippage = Some(0.01f64); + let checked_amount = Some(BigUint::from_str("1_640_000000000000000000").unwrap()); + let expected_min_amount = U256::from_str("1_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(), + false, + ) + .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 + "01", // transfer type + "0000000000000000000000000000", // padding + ] + .join(""); + + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata, expected_input); + write_calldata_to_file( + "test_single_swap_strategy_encoder_no_permit2", + hex_calldata.as_str(), + ); + } + + #[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("1_650_000000000000000000").unwrap()); + let slippage = Some(0.01f64); + let checked_amount = Some(BigUint::from_str("1_640_000000000000000000").unwrap()); + let expected_min_amount = U256::from_str("1_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); + write_calldata_to_file( + "test_single_swap_strategy_encoder_no_transfer_in", + hex_calldata.as_str(), + ); + } + + #[test] + fn test_single_swap_strategy_encoder_wrap() { + // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: eth(), + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1659881924818443699787").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_single_swap_strategy_encoder_wrap", hex_calldata.as_str()); + } + + #[test] + fn test_single_swap_strategy_encoder_unwrap() { + // Performs a single swap from DAI to WETH on a USV2 pool, unwrapping ETH at the end + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai.clone(), + token_out: weth(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: dai, + given_amount: BigUint::from_str("3_000_000000000000000000").unwrap(), + checked_token: eth(), + expected_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), + checked_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file( + "test_single_swap_strategy_encoder_unwrap", + hex_calldata.as_str(), + ); + } } - #[test] - fn test_split_encoding_strategy_curve_st_eth() { - // ETH ──(curve stETH pool)──> STETH + mod sequential { + use super::*; - let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); // ETH - let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); // STETH + #[test] + fn test_sequential_swap_strategy_encoder() { + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // Performs a sequential swap from WETH to USDC though WBTC using USV2 pools + // + // WETH ───(USV2)──> WBTC ───(USV2)──> USDC - let static_attributes = HashMap::from([( - "factory".to_string(), - Bytes::from( - "0x0000000000000000000000000000000000000000" - .as_bytes() - .to_vec(), - ), - )]); + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - let component = ProtocolComponent { - id: String::from("0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"), - protocol_system: String::from("vm:curve"), - static_attributes, - ..Default::default() - }; + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let swap = Swap { - component, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SplitSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - None, - Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), - ) - .unwrap(); + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: token_out, - expected_amount: None, - checked_amount: Some(BigUint::from_str("1").unwrap()), - slippage: None, - // Alice - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap], - ..Default::default() - }; + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_sequential_swap_strategy_encoder", hex_calldata.as_str()); + } - let (calldata, _) = encoder - .encode_strategy(solution) + #[test] + fn test_sequential_swap_strategy_encoder_no_permit2() { + // Performs a sequential swap from WETH to USDC though WBTC using USV2 pools + // + // WETH ───(USV2)──> WBTC ───(USV2)──> USDC + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + + let expected = String::from(concat!( + "e8a980d7", /* function selector */ + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token ou + "00000000000000000000000000000000000000000000000000000000018f61ec", /* min amount out */ + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "0000000000000000000000000000000000000000000000000000000000000100", /* length ple + * encode */ + "00000000000000000000000000000000000000000000000000000000000000a8", + // swap 1 + "0052", // swap length + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "bb2b8038a1640196fbe3e38816f3e67cba72d940", // component id + "004375dff511095cc5a197a54140a24efef3a416", // receiver (next pool) + "00", // zero to one + "01", // transfer type + // swap 2 + "0052", // swap length + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", // token in + "004375dff511095cc5a197a54140a24efef3a416", // component id + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver (final user) + "01", // zero to one + "05", // transfer type - None + "000000000000000000000000000000000000000000000000", // padding + )); + + assert_eq!(hex_calldata, expected); + write_calldata_to_file( + "test_sequential_swap_strategy_encoder_no_permit2", + hex_calldata.as_str(), + ); + } + + #[test] + fn test_sequential_strategy_cyclic_swap() { + // This test has start and end tokens that are the same + // The flow is: + // USDC -> WETH -> USDC using two pools + + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + // Create two Uniswap V3 pools for the cyclic swap + // USDC -> WETH (Pool 1) + let swap_usdc_weth = Swap { + component: ProtocolComponent { + id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 + * Pool 1 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(500).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0f64, + }; + + // WETH -> USDC (Pool 2) + let swap_weth_usdc = Swap { + component: ProtocolComponent { + id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 + * Pool 2 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) .unwrap(); - let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); + let solution = Solution { + exact_out: false, + given_token: usdc.clone(), + given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) + checked_token: usdc.clone(), + expected_amount: None, + checked_amount: Some(BigUint::from_str("99389294").unwrap()), /* Expected output + * from + * test */ + slippage: None, + swaps: vec![swap_usdc_weth, swap_weth_usdc], + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let hex_calldata = hex::encode(&calldata); + let expected_input = [ + "51bcc7b6", // selector + "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token + "0000000000000000000000000000000000000000000000000000000005ec8f6e", // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap action + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap action + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + + let expected_swaps = [ + "00000000000000000000000000000000000000000000000000000000000000d6", // length of ple encoded swaps without padding + "0069", // ple encoded swaps + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out + "0001f4", // pool fee + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id + "01", // zero2one + "02", // transfer type + "0069", // ple encoded swaps + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out + "000bb8", // pool fee + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id + "00", // zero2one + "00", // transfer type + "00000000000000000000", // padding + ] + .join(""); + + assert_eq!(hex_calldata[..456], expected_input); + assert_eq!(hex_calldata[1224..], expected_swaps); + write_calldata_to_file("test_sequential_strategy_cyclic_swap", hex_calldata.as_str()); + } + + mod optimized_transfers { + // In this module we test the ability to chain swaps or not. Different protocols are + // tested. The encoded data is used for solidity tests as well + use super::*; + + #[test] + fn test_uniswap_v3_uniswap_v2() { + // Note: This test does not assert anything. It is only used to obtain integration + // test data for our router solidity test. + // + // Performs a sequential swap from WETH to USDC though WBTC using USV3 and USV2 + // pools + // + // WETH ───(USV3)──> WBTC ───(USV2)──> USDC + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD".to_string(), + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + .unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_uniswap_v3_uniswap_v2", hex_calldata.as_str()); + } + + #[test] + fn test_uniswap_v3_uniswap_v3() { + // Note: This test does not assert anything. It is only used to obtain integration + // test data for our router solidity test. + // + // Performs a sequential swap from WETH to USDC though WBTC using USV3 pools + // There is no optimization between the two USV3 pools, this is currently not + // supported. + // + // WETH ───(USV3)──> WBTC ───(USV3)──> USDC + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD".to_string(), + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35".to_string(), + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + .unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_uniswap_v3_uniswap_v3", hex_calldata.as_str()); + } + + #[test] + fn test_uniswap_v3_curve() { + // Note: This test does not assert anything. It is only used to obtain integration + // test data for our router solidity test. + // + // Performs a sequential swap from WETH to USDT though WBTC using USV3 and curve + // pools + // + // WETH ───(USV3)──> WBTC ───(curve)──> USDT + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdt = Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD".to_string(), + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + + let swap_wbtc_usdt = Swap { + component: ProtocolComponent { + id: String::from("0xD51a44d3FaE010294C616388b506AcdA1bfAAE46"), + protocol_system: String::from("vm:curve"), + static_attributes: { + let mut attrs: HashMap = HashMap::new(); + attrs.insert( + "factory".into(), + Bytes::from( + "0x0000000000000000000000000000000000000000" + .as_bytes() + .to_vec(), + ), + ); + attrs + }, + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdt.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdt, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + .unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdt], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_uniswap_v3_curve", hex_calldata.as_str()); + } + + #[test] + fn test_balancer_v2_uniswap_v2() { + // Note: This test does not assert anything. It is only used to obtain integration + // test data for our router solidity test. + // + // Performs a sequential swap from WETH to USDC though WBTC using balancer and USV2 + // pools + // + // WETH ───(balancer)──> WBTC ───(USV2)──> USDC + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e" + .to_string(), + protocol_system: "vm:balancer_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + .unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_balancer_v2_uniswap_v2", hex_calldata.as_str()); + } + + #[test] + fn test_multi_protocol() { + // Note: This test does not assert anything. It is only used to obtain integration + // test data for our router solidity test. + // + // Performs the following swap: + // + // DAI ─(USV2)-> WETH ─(bal)─> WBTC ─(curve)─> USDT ─(ekubo)─> USDC ─(USV4)─> ETH + + let weth = weth(); + let eth = eth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let usdt = Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(); + let dai = Bytes::from_str("0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(); + + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234" + .to_string(); + + let usv2_swap_dai_weth = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai.clone(), + token_out: weth.clone(), + split: 0f64, + }; + + let balancer_swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e" + .to_string(), + protocol_system: "vm:balancer_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + + let curve_swap_wbtc_usdt = Swap { + component: ProtocolComponent { + id: String::from("0xD51a44d3FaE010294C616388b506AcdA1bfAAE46"), + protocol_system: String::from("vm:curve"), + static_attributes: { + let mut attrs: HashMap = HashMap::new(); + attrs.insert( + "factory".into(), + Bytes::from( + "0x0000000000000000000000000000000000000000" + .as_bytes() + .to_vec(), + ), + ); + attrs + }, + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdt.clone(), + split: 0f64, + }; + + // Ekubo + + let component = ProtocolComponent { + // All Ekubo swaps go through the core contract - not necessary to specify pool + // id for test + protocol_system: "ekubo_v2".to_string(), + // 0.0025% fee & 0.005% base pool + static_attributes: HashMap::from([ + ("fee".to_string(), Bytes::from(461168601842738_u64)), + ("tick_spacing".to_string(), Bytes::from(50_u32)), + ("extension".to_string(), Bytes::zero(20)), + ]), + ..Default::default() + }; + let ekubo_swap_usdt_usdc = Swap { + component, + token_in: usdt.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + + // USV4 + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let usv4_swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + // Put all components together + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: dai, + given_amount: BigUint::from_str("1500_000000000000000000").unwrap(), + checked_token: eth, + expected_amount: None, + checked_amount: Some(BigUint::from_str("732214216964381330").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + .unwrap(), + swaps: vec![ + usv2_swap_dai_weth, + balancer_swap_weth_wbtc, + curve_swap_wbtc_usdt, + ekubo_swap_usdt_usdc, + usv4_swap_usdc_eth, + ], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_multi_protocol", hex_calldata.as_str()); + } + } + } + + mod split { + use super::*; + + #[test] + fn test_split_swap_strategy_encoder() { + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // ┌──(USV2)──> WBTC ───(USV2)──> USDC + // WETH ─┤ + // └──(USV2)──> DAI ───(USV2)──> USDC + // + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = weth(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_dai = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.5f64, + }; + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + // This represents the remaining 50%, but to avoid any rounding errors we set this + // to 0 to signify "the remainder of the WETH value". It should + // still be very close to 50% + split: 0f64, + }; + let swap_dai_usdc = Swap { + component: ProtocolComponent { + id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_split_swap_strategy_encoder", hex_calldata.as_str()); + } + + #[test] + fn test_split_input_cyclic_swap() { + // 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 ─┘ + + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + // USDC -> WETH (Pool 1) - 60% of input + let swap_usdc_weth_pool1 = Swap { + component: ProtocolComponent { + id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 + * Pool 1 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(500).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0.6f64, // 60% of input + }; + + // USDC -> WETH (Pool 2) - 40% of input (remaining) + let swap_usdc_weth_pool2 = Swap { + component: ProtocolComponent { + id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 + * Pool 2 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0f64, // Remaining 40% + }; + + // WETH -> USDC (Pool 2) + let swap_weth_usdc_pool2 = Swap { + component: ProtocolComponent { + id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2 + * Pool 2 */ + protocol_system: "uniswap_v2".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.clone(), + split: 0.0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key.clone()), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: usdc.clone(), + given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) + checked_token: usdc.clone(), + expected_amount: None, + checked_amount: Some(BigUint::from_str("99574171").unwrap()), /* Expected output + * from + * test */ + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + slippage: None, + swaps: vec![swap_usdc_weth_pool1, swap_usdc_weth_pool2, swap_weth_usdc_pool2], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = hex::encode(&calldata); + let expected_input = [ + "7c553846", // selector + "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token + "0000000000000000000000000000000000000000000000000000000005ef619b", // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap action + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap action + "0000000000000000000000000000000000000000000000000000000000000002", // tokens length + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + let expected_swaps = [ + "0000000000000000000000000000000000000000000000000000000000000139", // length of ple encoded swaps without padding + "006e", // ple encoded swaps + "00", // token in index + "01", // token out index + "999999", // split + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out + "0001f4", // pool fee + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id + "01", // zero2one + "02", // transfer type + "006e", // ple encoded swaps + "00", // token in index + "01", // token out index + "000000", // split + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out + "000bb8", // pool fee + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id + "01", // zero2one + "02", // transfer type + "0057", // ple encoded swaps + "01", // token in index + "00", // token out index + "000000", // split + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address, + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id, + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "00", // zero2one + "00", // transfer type + "00000000000000" // padding + ] + .join(""); + assert_eq!(hex_calldata[..520], expected_input); + assert_eq!(hex_calldata[1288..], expected_swaps); + write_calldata_to_file("test_split_input_cyclic_swap", hex_calldata.as_str()); + } + + #[test] + fn test_split_output_cyclic_swap() { + // This test has start and end tokens that are the same + // The flow is: + // ┌─── (USV3, 60% split) ───┐ + // │ │ + // USDC ──(USV2) ── WETH──| ├─> USDC + // │ │ + // └─── (USV3, 40% split) ───┘ + + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_usdc_weth_v2 = Swap { + component: ProtocolComponent { + id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2 + protocol_system: "uniswap_v2".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(500).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0.0f64, + }; + + let swap_weth_usdc_v3_pool1 = Swap { + component: ProtocolComponent { + id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 + * Pool 1 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(500).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.clone(), + split: 0.6f64, + }; + + let swap_weth_usdc_v3_pool2 = Swap { + component: ProtocolComponent { + id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 + * Pool 2 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.clone(), + split: 0.0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key.clone()), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: usdc.clone(), + given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) + checked_token: usdc.clone(), + expected_amount: None, + checked_amount: Some(BigUint::from_str("99025908").unwrap()), /* Expected output + * from + * test */ + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + slippage: None, + swaps: vec![swap_usdc_weth_v2, swap_weth_usdc_v3_pool1, swap_weth_usdc_v3_pool2], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = hex::encode(&calldata); + let expected_input = [ + "7c553846", // selector + "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token + "0000000000000000000000000000000000000000000000000000000005e703f4", // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap action + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap action + "0000000000000000000000000000000000000000000000000000000000000002", // tokens length + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + + let expected_swaps = [ + "0000000000000000000000000000000000000000000000000000000000000139", // length of ple encoded swaps without padding + "0057", // ple encoded swaps + "00", // token in index + "01", // token out index + "000000", // split + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in + "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "01", // zero2one + "02", // transfer type + "006e", // ple encoded swaps + "01", // token in index + "00", // token out index + "999999", // split + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out + "0001f4", // pool fee + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id + "00", // zero2one + "00", // transfer type + "006e", // ple encoded swaps + "01", // token in index + "00", // token out index + "000000", // split + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out + "000bb8", // pool fee + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id + "00", // zero2one + "00", // transfer type + "00000000000000" // padding + ] + .join(""); + + assert_eq!(hex_calldata[..520], expected_input); + assert_eq!(hex_calldata[1288..], expected_swaps); + write_calldata_to_file("test_split_output_cyclic_swap", hex_calldata.as_str()); + } + } + + mod protocol_integration { + // in this module we test protocol specific logic by creating the calldata that then is + // used in the solidity tests + use super::*; + + #[test] + fn test_single_encoding_strategy_ekubo() { + // ETH ──(EKUBO)──> USDC + + let token_in = Bytes::from(Address::ZERO.as_slice()); + let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + + let static_attributes = HashMap::from([ + ("fee".to_string(), Bytes::from(0_u64)), + ("tick_spacing".to_string(), Bytes::from(0_u32)), + ( + "extension".to_string(), + Bytes::from("0x51d02a5948496a67827242eabc5725531342527c"), + ), /* Oracle */ + ]); + + let component = ProtocolComponent { + // All Ekubo swaps go through the core contract - not necessary to specify pool id + // for test + protocol_system: "ekubo_v2".to_string(), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap(), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: token_out, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1000").unwrap()), + slippage: None, + // Alice + 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 hex_calldata = encode(&calldata); + write_calldata_to_file("test_single_encoding_strategy_ekubo", hex_calldata.as_str()); + } + + #[test] + fn test_single_encoding_strategy_usv4_eth_in() { + // Performs a single swap from ETH to PEPE using a USV4 pool + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // ETH ───(USV4)──> PEPE + // + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let eth = eth(); + let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); + + let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); + let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); + let mut static_attributes_eth_pepe: HashMap = HashMap::new(); + static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); + static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); + + let swap_eth_pepe = Swap { + component: ProtocolComponent { + id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_eth_pepe, + ..Default::default() + }, + token_in: eth.clone(), + token_out: pepe.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: eth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: pepe, + expected_amount: None, + checked_amount: Some(BigUint::from_str("152373460199848577067005852").unwrap()), + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_eth_pepe], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let hex_calldata = encode(&calldata); + + write_calldata_to_file( + "test_single_encoding_strategy_usv4_eth_in", + hex_calldata.as_str(), + ); + } + + #[test] + fn test_single_encoding_strategy_usv4_eth_out() { + // Performs a single swap from USDC to ETH using a USV4 pool + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // USDC ───(USV4)──> ETH + // + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let eth = eth(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("3000_000000").unwrap(), + checked_token: eth, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1117254495486192350").unwrap()), + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_usdc_eth], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + write_calldata_to_file( + "test_single_encoding_strategy_usv4_eth_out", + hex_calldata.as_str(), + ); + } + + #[test] + fn test_sequential_encoding_strategy_usv4() { + // Performs a sequential swap from USDC to PEPE though ETH using two consecutive USV4 + // pools + // + // USDC ──(USV4)──> ETH ───(USV4)──> PEPE + // + + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let eth = eth(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); + + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); + let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); + let mut static_attributes_eth_pepe: HashMap = HashMap::new(); + static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); + static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); + + let swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + let swap_eth_pepe = Swap { + component: ProtocolComponent { + id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_eth_pepe, + ..Default::default() + }, + token_in: eth.clone(), + token_out: pepe.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("1000_000000").unwrap(), + checked_token: pepe, + expected_amount: None, + checked_amount: Some(BigUint::from_str("97191013220606467325121599").unwrap()), + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_usdc_eth, swap_eth_pepe], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let expected_input = [ + "51bcc7b6", // Function selector + "000000000000000000000000000000000000000000000000000000003b9aca00", // amount in + "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in + "0000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933", // token out + "0000000000000000000000000000000000000000005064ff624d54346285543f", // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + + // after this there is the permit and because of the deadlines (that depend on block + // time) it's hard to assert + + let expected_swaps = String::from(concat!( + // length of ple encoded swaps without padding + "0000000000000000000000000000000000000000000000000000000000000088", + // ple encoded swaps + "0086", // Swap length + // Swap data header + "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address + // Protocol data + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // group token in + "6982508145454ce325ddbe47a25d4ec3d2311933", // group token in + "00", // zero2one + "02", // transfer type (transfer to router) + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + // First pool params + "0000000000000000000000000000000000000000", // intermediary token (ETH) + "000bb8", // fee + "00003c", // tick spacing + // Second pool params + "6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE) + "0061a8", // fee + "0001f4", // tick spacing + "000000000000000000000000000000000000000000000000" // padding + )); + + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata[..456], expected_input); + assert_eq!(hex_calldata[1224..], expected_swaps); + write_calldata_to_file("test_sequential_encoding_strategy_usv4", hex_calldata.as_str()); + } + + #[test] + fn test_single_encoding_strategy_curve() { + // UWU ──(curve 2 crypto pool)──> WETH + + let token_in = Bytes::from("0x55C08ca52497e2f1534B59E2917BF524D4765257"); // UWU + let token_out = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH + + let static_attributes = HashMap::from([( + "factory".to_string(), + Bytes::from( + "0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f" + .as_bytes() + .to_vec(), + ), + )]); + + let component = ProtocolComponent { + id: String::from("0x77146B0a1d08B6844376dF6d9da99bA7F1b19e71"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.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(), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: token_out, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1").unwrap()), + slippage: None, + // Alice + 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 hex_calldata = encode(&calldata); + write_calldata_to_file("test_single_encoding_strategy_curve", hex_calldata.as_str()); + } + + #[test] + fn test_single_encoding_strategy_curve_st_eth() { + // ETH ──(curve stETH pool)──> STETH + + let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); // ETH + let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); // STETH + + let static_attributes = HashMap::from([( + "factory".to_string(), + Bytes::from( + "0x0000000000000000000000000000000000000000" + .as_bytes() + .to_vec(), + ), + )]); + + let component = ProtocolComponent { + id: String::from("0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.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(), + false, + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: token_out, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1").unwrap()), + slippage: None, + // Alice + 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 hex_calldata = encode(&calldata); + write_calldata_to_file( + "test_single_encoding_strategy_curve_st_eth", + hex_calldata.as_str(), + ); + } } } diff --git a/src/encoding/evm/strategy_encoder/strategy_validators.rs b/src/encoding/evm/strategy_encoder/strategy_validators.rs index e7ae722..4ace21f 100644 --- a/src/encoding/evm/strategy_encoder/strategy_validators.rs +++ b/src/encoding/evm/strategy_encoder/strategy_validators.rs @@ -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> = 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> = 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; diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs new file mode 100644 index 0000000..62bfca1 --- /dev/null +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -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); + } +} diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 130e4cd..a71e187 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -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, - blockchain: tycho_common::models::Chain, - ) -> Result { - let chain = Chain::from(blockchain); + pub fn new(executors_file_path: Option, chain: Chain) -> Result { 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 { diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index b38a3cf..9a82bfc 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -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,405 +644,442 @@ mod tests { }; use super::*; + use crate::encoding::models::TransferType; - #[test] - fn test_encode_uniswap_v2() { - let usv2_pool = ProtocolComponent { - id: String::from("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), - ..Default::default() - }; + mod uniswap_v2 { + use super::*; + #[test] + fn test_encode_uniswap_v2() { + let usv2_pool = ProtocolComponent { + id: String::from("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), + ..Default::default() + }; - let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - let swap = Swap { - component: usv2_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - receiver: Bytes::from("0x0000000000000000000000000000000000000001"), - exact_out: false, - router_address: Some(Bytes::zero(20)), - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - }; - let encoder = UniswapV2SwapEncoder::new( - String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - TychoCoreChain::Ethereum.into(), - None, - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) + let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); + let swap = Swap { + component: usv2_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + receiver: Bytes::from("0x0000000000000000000000000000000000000001"), + exact_out: false, + 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"), + TychoCoreChain::Ethereum.into(), + None, + ) .unwrap(); - let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // in token - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // component id - "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", - // receiver - "0000000000000000000000000000000000000001", - // zero for one - "00", - )) - ); - } - #[test] - fn test_encode_uniswap_v3() { - let fee = BigInt::from(500); - let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); - - let usv3_pool = ProtocolComponent { - id: String::from("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), - static_attributes, - ..Default::default() - }; - let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - let swap = Swap { - component: usv3_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - receiver: Bytes::from("0x0000000000000000000000000000000000000001"), - exact_out: false, - router_address: Some(Bytes::zero(20)), - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - }; - let encoder = UniswapV3SwapEncoder::new( - String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - TychoCoreChain::Ethereum.into(), - None, - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) - .unwrap(); - let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // in token - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // out token - "6b175474e89094c44da98b954eedeac495271d0f", - // fee - "0001f4", - // receiver - "0000000000000000000000000000000000000001", - // pool id - "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", - // zero for one - "00", - )) - ); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + assert_eq!( + hex_swap, + String::from(concat!( + // in token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // component id + "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", + // receiver + "0000000000000000000000000000000000000001", + // zero for one + "00", + // transfer type (transfer) + "00", + )) + ); + } } - #[test] - fn test_encode_balancer_v2() { - let balancer_pool = ProtocolComponent { - id: String::from("0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014"), - protocol_system: String::from("vm:balancer_v2"), - ..Default::default() - }; - let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); - let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D"); - let swap = Swap { - component: balancer_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), - exact_out: false, - router_address: Some(Bytes::zero(20)), - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - }; - let encoder = BalancerV2SwapEncoder::new( - String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - TychoCoreChain::Ethereum.into(), - Some(HashMap::from([( - "vault_address".to_string(), - "0xba12222222228d8ba445958a75a0704d566bf2c8".to_string(), - )])), - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) - .unwrap(); - let hex_swap = encode(&encoded_swap); + mod uniswap_v3 { + use super::*; + #[test] + fn test_encode_uniswap_v3() { + let fee = BigInt::from(500); + let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // token out - "ba100000625a3754423978a60c9317c58a424e3d", - // pool id - "5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", - // receiver - "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - // approval needed - "01" - )) - ); + let usv3_pool = ProtocolComponent { + id: String::from("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); + let swap = Swap { + component: usv3_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + receiver: Bytes::from("0x0000000000000000000000000000000000000001"), + exact_out: false, + 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"), + TychoCoreChain::Ethereum.into(), + None, + ) + .unwrap(); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + assert_eq!( + hex_swap, + String::from(concat!( + // in token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // out token + "6b175474e89094c44da98b954eedeac495271d0f", + // fee + "0001f4", + // receiver + "0000000000000000000000000000000000000001", + // pool id + "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", + // zero for one + "00", + // transfer type (transfer) + "00", + )) + ); + } } - #[test] - fn test_encode_uniswap_v4_simple_swap() { - let fee = BigInt::from(100); - let tick_spacing = BigInt::from(1); - let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE - let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + mod balancer_v2 { + use super::*; + use crate::encoding::evm::utils::write_calldata_to_file; - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("key_lp_fee".into(), Bytes::from(fee.to_signed_bytes_be())); - static_attributes - .insert("tick_spacing".into(), Bytes::from(tick_spacing.to_signed_bytes_be())); - - let usv4_pool = ProtocolComponent { - // Pool manager - id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), - static_attributes, - ..Default::default() - }; - let swap = Swap { - component: usv4_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - // The receiver address was taken from `address(uniswapV4Exposed)` in the - // UniswapV4Executor.t.sol - receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), - 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(), - }; - let encoder = UniswapV4SwapEncoder::new( - String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), - TychoCoreChain::Ethereum.into(), - None, - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) + #[test] + fn test_encode_balancer_v2() { + let balancer_pool = ProtocolComponent { + id: String::from( + "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + ), + protocol_system: String::from("vm:balancer_v2"), + ..Default::default() + }; + let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D"); + let swap = Swap { + component: balancer_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + 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"), + TychoCoreChain::Ethereum.into(), + Some(HashMap::from([( + "vault_address".to_string(), + "0xba12222222228d8ba445958a75a0704d566bf2c8".to_string(), + )])), + ) .unwrap(); - let hex_swap = encode(&encoded_swap); - println!("{}", hex_swap); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // group token in - "4c9edd5852cd905f086c759e8383e09bff1e68b3", - // group token out - "dac17f958d2ee523a2206206994597c13d831ec7", - // zero for one - "01", - // executor address - "f62849f9a0b5bf2913b396098f7c7019b51a820a", - // pool params: - // - intermediary token - "dac17f958d2ee523a2206206994597c13d831ec7", - // - fee - "000064", - // - tick spacing - "000001" - )) - ); + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // token out + "ba100000625a3754423978a60c9317c58a424e3d", + // pool id + "5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + // receiver + "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", + // approval needed + "01", + // transfer type + "05" + )) + ); + write_calldata_to_file("test_encode_balancer_v2", hex_swap.as_str()); + } } - #[test] - fn test_encode_uniswap_v4_second_swap() { - let fee = BigInt::from(3000); - let tick_spacing = BigInt::from(60); - let group_token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE - let token_in = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT - let token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC + mod uniswap_v4 { + use super::*; + use crate::encoding::evm::utils::write_calldata_to_file; - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("key_lp_fee".into(), Bytes::from(fee.to_signed_bytes_be())); - static_attributes - .insert("tick_spacing".into(), Bytes::from(tick_spacing.to_signed_bytes_be())); + #[test] + fn test_encode_uniswap_v4_simple_swap() { + let fee = BigInt::from(100); + let tick_spacing = BigInt::from(1); + let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE + let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT - let usv4_pool = ProtocolComponent { - id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), - static_attributes, - ..Default::default() - }; + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("key_lp_fee".into(), Bytes::from(fee.to_signed_bytes_be())); + static_attributes + .insert("tick_spacing".into(), Bytes::from(tick_spacing.to_signed_bytes_be())); - let swap = Swap { - component: usv4_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; + let usv4_pool = ProtocolComponent { + // Pool manager + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes, + ..Default::default() + }; + let swap = Swap { + component: usv4_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // 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")), - let encoding_context = EncodingContext { - receiver: Bytes::from("0x0000000000000000000000000000000000000001"), - exact_out: false, - router_address: Some(Bytes::zero(20)), - group_token_in: group_token_in.clone(), - // Token out is the same as the group token out - group_token_out: token_out.clone(), - }; - - let encoder = UniswapV4SwapEncoder::new( - String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - TychoCoreChain::Ethereum.into(), - None, - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + transfer_type: TransferType::TransferToProtocol, + }; + let encoder = UniswapV4SwapEncoder::new( + String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), + TychoCoreChain::Ethereum.into(), + None, + ) .unwrap(); - let hex_swap = encode(&encoded_swap); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // pool params: - // - intermediary token (20 bytes) - "2260fac5e5542a773aa44fbcfedf7c193bc2c599", - // - fee (3 bytes) - "000bb8", - // - tick spacing (3 bytes) - "00003c" - )) - ); + assert_eq!( + hex_swap, + String::from(concat!( + // group token in + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // group token out + "dac17f958d2ee523a2206206994597c13d831ec7", + // zero for one + "01", + // transfer type + "00", + // receiver + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // pool params: + // - intermediary token + "dac17f958d2ee523a2206206994597c13d831ec7", + // - fee + "000064", + // - tick spacing + "000001" + )) + ); + write_calldata_to_file("test_encode_uniswap_v4_simple_swap", hex_swap.as_str()); + } + + #[test] + fn test_encode_uniswap_v4_second_swap() { + let fee = BigInt::from(3000); + let tick_spacing = BigInt::from(60); + let group_token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE + let token_in = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + let token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC + + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("key_lp_fee".into(), Bytes::from(fee.to_signed_bytes_be())); + static_attributes + .insert("tick_spacing".into(), Bytes::from(tick_spacing.to_signed_bytes_be())); + + let usv4_pool = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component: usv4_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let encoding_context = EncodingContext { + receiver: Bytes::from("0x0000000000000000000000000000000000000001"), + exact_out: false, + router_address: Some(Bytes::zero(20)), + 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( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + TychoCoreChain::Ethereum.into(), + None, + ) + .unwrap(); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // pool params: + // - intermediary token (20 bytes) + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // - fee (3 bytes) + "000bb8", + // - tick spacing (3 bytes) + "00003c" + )) + ); + } + + #[test] + fn test_encode_uniswap_v4_sequential_swap() { + let usde_address = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); + let usdt_address = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); + let wbtc_address = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); + let router_address = Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"); + + // The context is the same for both swaps, since the group token in and out are the same + let context = EncodingContext { + // 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 + let usde_usdt_fee = BigInt::from(100); + let usde_usdt_tick_spacing = BigInt::from(1); + + let mut usde_usdt_static_attributes: HashMap = HashMap::new(); + usde_usdt_static_attributes + .insert("key_lp_fee".into(), Bytes::from(usde_usdt_fee.to_signed_bytes_be())); + usde_usdt_static_attributes.insert( + "tick_spacing".into(), + Bytes::from(usde_usdt_tick_spacing.to_signed_bytes_be()), + ); + + let usde_usdt_component = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes: usde_usdt_static_attributes, + ..Default::default() + }; + + // Setup - Second sequence: USDT -> WBTC + let usdt_wbtc_fee = BigInt::from(3000); + let usdt_wbtc_tick_spacing = BigInt::from(60); + + let mut usdt_wbtc_static_attributes: HashMap = HashMap::new(); + usdt_wbtc_static_attributes + .insert("key_lp_fee".into(), Bytes::from(usdt_wbtc_fee.to_signed_bytes_be())); + usdt_wbtc_static_attributes.insert( + "tick_spacing".into(), + Bytes::from(usdt_wbtc_tick_spacing.to_signed_bytes_be()), + ); + + let usdt_wbtc_component = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes: usdt_wbtc_static_attributes, + ..Default::default() + }; + + let initial_swap = Swap { + component: usde_usdt_component, + token_in: usde_address.clone(), + token_out: usdt_address.clone(), + split: 0f64, + }; + + let second_swap = Swap { + component: usdt_wbtc_component, + token_in: usdt_address, + token_out: wbtc_address.clone(), + split: 0f64, + }; + + let encoder = UniswapV4SwapEncoder::new( + String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), + TychoCoreChain::Ethereum.into(), + None, + ) + .unwrap(); + let initial_encoded_swap = encoder + .encode_swap(initial_swap, context.clone()) + .unwrap(); + let second_encoded_swap = encoder + .encode_swap(second_swap, context) + .unwrap(); + + let combined_hex = + format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); + + assert_eq!( + combined_hex, + String::from(concat!( + // group_token in + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // group_token out + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // zero for one + "01", + // transfer type + "00", + // receiver + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // pool params: + // - intermediary token USDT + "dac17f958d2ee523a2206206994597c13d831ec7", + // - fee + "000064", + // - tick spacing + "000001", + // - intermediary token WBTC + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // - fee + "000bb8", + // - tick spacing + "00003c" + )) + ); + write_calldata_to_file("test_encode_uniswap_v4_sequential_swap", combined_hex.as_str()); + } } - - #[test] - fn test_encode_uniswap_v4_sequential_swap() { - let usde_address = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); - 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(), - exact_out: false, - router_address: Some(router_address.clone()), - group_token_in: usde_address.clone(), - group_token_out: wbtc_address.clone(), - }; - - // Setup - First sequence: USDE -> USDT - let usde_usdt_fee = BigInt::from(100); - let usde_usdt_tick_spacing = BigInt::from(1); - - let mut usde_usdt_static_attributes: HashMap = HashMap::new(); - usde_usdt_static_attributes - .insert("key_lp_fee".into(), Bytes::from(usde_usdt_fee.to_signed_bytes_be())); - usde_usdt_static_attributes.insert( - "tick_spacing".into(), - Bytes::from(usde_usdt_tick_spacing.to_signed_bytes_be()), - ); - - let usde_usdt_component = ProtocolComponent { - id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), - static_attributes: usde_usdt_static_attributes, - ..Default::default() - }; - - // Setup - Second sequence: USDT -> WBTC - let usdt_wbtc_fee = BigInt::from(3000); - let usdt_wbtc_tick_spacing = BigInt::from(60); - - let mut usdt_wbtc_static_attributes: HashMap = HashMap::new(); - usdt_wbtc_static_attributes - .insert("key_lp_fee".into(), Bytes::from(usdt_wbtc_fee.to_signed_bytes_be())); - usdt_wbtc_static_attributes.insert( - "tick_spacing".into(), - Bytes::from(usdt_wbtc_tick_spacing.to_signed_bytes_be()), - ); - - let usdt_wbtc_component = ProtocolComponent { - id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), - static_attributes: usdt_wbtc_static_attributes, - ..Default::default() - }; - - let initial_swap = Swap { - component: usde_usdt_component, - token_in: usde_address.clone(), - token_out: usdt_address.clone(), - split: 0f64, - }; - - let second_swap = Swap { - component: usdt_wbtc_component, - token_in: usdt_address, - token_out: wbtc_address.clone(), - split: 0f64, - }; - - let encoder = UniswapV4SwapEncoder::new( - String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), - TychoCoreChain::Ethereum.into(), - None, - ) - .unwrap(); - let initial_encoded_swap = encoder - .encode_swap(initial_swap, context.clone()) - .unwrap(); - let second_encoded_swap = encoder - .encode_swap(second_swap, context) - .unwrap(); - - let combined_hex = - format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); - - assert_eq!( - combined_hex, - String::from(concat!( - // group_token in - "4c9edd5852cd905f086c759e8383e09bff1e68b3", - // group_token out - "2260fac5e5542a773aa44fbcfedf7c193bc2c599", - // zero for one - "01", - // executor address - "f62849f9a0b5bf2913b396098f7c7019b51a820a", - // pool params: - // - intermediary token USDT - "dac17f958d2ee523a2206206994597c13d831ec7", - // - fee - "000064", - // - tick spacing - "000001", - // - intermediary token WBTC - "2260fac5e5542a773aa44fbcfedf7c193bc2c599", - // - fee - "000bb8", - // - tick spacing - "00003c" - )) - ); - } - 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,15 +1127,18 @@ mod tests { assert_eq!( hex_swap, - RECEIVER.to_string() + - concat!( - // group token in - "0000000000000000000000000000000000000000", - // token out 1st swap - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // pool config 1st swap - "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", - ), + concat!( + // transfer type + "00", + // receiver + "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6", + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + ), ); } @@ -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,24 +1204,27 @@ mod tests { let combined_hex = format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); - println!("{}", combined_hex); - assert_eq!( combined_hex, - RECEIVER.to_string() + - concat!( - // group token in - "0000000000000000000000000000000000000000", - // token out 1st swap - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // pool config 1st swap - "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", - // token out 2nd swap - "dac17f958d2ee523a2206206994597c13d831ec7", - // pool config 2nd swap - "00000000000000000000000000000000000000000001a36e2eb1c43200000032", - ), + // transfer type + concat!( + // transfer type + "00", + // receiver + "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6", + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + // token out 2nd swap + "dac17f958d2ee523a2206206994597c13d831ec7", + // pool config 2nd swap + "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" )) ); } diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs deleted file mode 100644 index 85f541f..0000000 --- a/src/encoding/evm/tycho_encoder.rs +++ /dev/null @@ -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, - 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, - ) -> Result { - 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::>() - .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, - ) -> Result, EncodingError> { - let mut transactions: Vec = 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, 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> { - None - } - fn clone_box(&self) -> Box { - 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() - ) - ); - } -} diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs new file mode 100644 index 0000000..c6c5692 --- /dev/null +++ b/src/encoding/evm/tycho_encoders.rs @@ -0,0 +1,1075 @@ +use std::{collections::HashSet, str::FromStr}; + +use num_bigint::BigUint; +use tycho_common::Bytes; + +use crate::encoding::{ + errors::EncodingError, + evm::{ + group_swaps::group_swaps, + strategy_encoder::strategy_encoders::{ + SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, + }, + swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + }, + models::{Chain, EncodingContext, NativeAction, Solution, Transaction, TransferType}, + strategy_encoder::StrategyEncoder, + tycho_encoder::TychoEncoder, +}; + +/// Encodes solutions to be used by the TychoRouter. +/// +/// # Fields +/// * `single_swap_strategy`: Encoder for single swaps +/// * `sequential_swap_strategy`: Encoder for sequential swaps +/// * `split_swap_strategy`: Encoder for split swaps +/// * `native_address`: Address of the chain's native token +/// * `wrapped_address`: Address of the chain's wrapped native token +pub struct TychoRouterEncoder { + single_swap_strategy: SingleSwapStrategyEncoder, + sequential_swap_strategy: SequentialSwapStrategyEncoder, + split_swap_strategy: SplitSwapStrategyEncoder, + native_address: Bytes, + wrapped_address: Bytes, +} + +impl TychoRouterEncoder { + pub fn new( + chain: Chain, + 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()?; + Ok(TychoRouterEncoder { + single_swap_strategy: SingleSwapStrategyEncoder::new( + chain.clone(), + 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, + }) + } +} + +impl TychoEncoder for TychoRouterEncoder { + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError> { + let mut transactions: Vec = Vec::new(); + for solution in solutions.iter() { + self.validate_solution(solution)?; + let (contract_interaction, target_address) = if solution.swaps.len() == 1 { + self.single_swap_strategy + .encode_strategy(solution.clone())? + } else if solution + .swaps + .iter() + .all(|swap| swap.split == 0.0) + { + self.sequential_swap_strategy + .encode_strategy(solution.clone())? + } else { + self.split_swap_strategy + .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) + } + + /// 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::>() + .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(()) + } +} + +/// Represents an encoder for one swap to be executed directly against an Executor. +/// +/// This is useful when you want to bypass the Tycho Router, use your own Router contract and +/// just need the calldata for a particular swap. +/// +/// # Fields +/// * `swap_encoder_registry`: Registry of swap encoders +/// * `native_address`: Address of the chain's native token +pub struct TychoExecutorEncoder { + swap_encoder_registry: SwapEncoderRegistry, + native_address: Bytes, +} + +impl TychoExecutorEncoder { + pub fn new( + chain: Chain, + swap_encoder_registry: SwapEncoderRegistry, + ) -> Result { + let native_address = chain.native_token()?; + Ok(TychoExecutorEncoder { swap_encoder_registry, native_address }) + } + + fn encode_executor_calldata( + &self, + solution: Solution, + ) -> Result<(Vec, Bytes), EncodingError> { + let grouped_swaps = group_swaps(solution.clone().swaps); + let number_of_groups = grouped_swaps.len(); + if number_of_groups > 1 { + return Err(EncodingError::InvalidInput(format!( + "Tycho executor encoder only supports one swap. Found {number_of_groups}" + ))) + } + + let grouped_swap = grouped_swaps + .first() + .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; + + let receiver = solution.receiver; + + let swap_encoder = self + .swap_encoder_registry + .get_encoder(&grouped_swap.protocol_system) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {}", + grouped_swap.protocol_system + )) + })?; + + let mut grouped_protocol_data: Vec = vec![]; + for swap in grouped_swap.swaps.iter() { + let encoding_context = EncodingContext { + receiver: receiver.clone(), + exact_out: solution.exact_out, + router_address: None, + 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())?; + grouped_protocol_data.extend(protocol_data); + } + + let executor_address = Bytes::from_str(swap_encoder.executor_address()) + .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; + + Ok((grouped_protocol_data, executor_address)) + } +} + +impl TychoEncoder for TychoExecutorEncoder { + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError> { + let mut transactions: Vec = Vec::new(); + let solution = solutions + .first() + .ok_or(EncodingError::FatalError("No solutions found".to_string()))?; + self.validate_solution(solution)?; + + let (contract_interaction, target_address) = + self.encode_executor_calldata(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) + } + + /// 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. + 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(), + )); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCommonChain}; + + use super::*; + use crate::encoding::models::Swap; + + 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() + } + + fn get_swap_encoder_registry() -> SwapEncoderRegistry { + SwapEncoderRegistry::new( + Some("config/test_executor_addresses.json".to_string()), + TychoCommonChain::Ethereum.into(), + ) + .unwrap() + } + + mod router_encoder { + use super::*; + + fn get_mocked_tycho_router_encoder() -> TychoRouterEncoder { + TychoRouterEncoder::new( + TychoCommonChain::Ethereum.into(), + get_swap_encoder_registry(), + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, + ) + .unwrap() + } + #[test] + fn test_encode_router_calldata() { + let encoder = get_mocked_tycho_router_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(), + checked_token: dai(), + swaps: vec![swap], + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let transactions = encoder.encode_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].to, + Bytes::from_str("0x3ede3eca2a72b3aecc820e955b36f38437d01395").unwrap() + ); + } + + #[test] + fn test_validate_fails_for_exact_out() { + let encoder = get_mocked_tycho_router_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_router_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_router_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_router_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_router_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_router_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_router_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_router_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_router_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_router_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_router_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_router_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() + ) + ); + } + } + + mod executor_encoder { + use std::{collections::HashMap, str::FromStr}; + + use alloy::hex::encode; + use num_bigint::{BigInt, BigUint}; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; + + use super::*; + use crate::encoding::models::{Solution, Swap}; + + #[test] + fn test_executor_encoder_encode() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCommonChain::Ethereum.into(), swap_encoder_registry) + .unwrap(); + + let token_in = weth(); + let token_out = dai(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from(1000000000000000000u64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + checked_token: token_out, + checked_amount: None, + sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), + swaps: vec![swap], + slippage: None, + native_action: None, + }; + + let transactions = encoder + .encode_calldata(vec![solution]) + .unwrap(); + let transaction = transactions + .first() + .expect("Expected at least one transaction"); + let hex_protocol_data = encode(&transaction.data); + assert_eq!( + transaction.to, + Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() + ); + assert_eq!( + hex_protocol_data, + String::from(concat!( + // in token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // component id + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", + // receiver + "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", + // zero for one + "00", + // transfer type + "00", + )) + ); + } + + #[test] + fn test_executor_encoder_too_many_swaps() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCommonChain::Ethereum.into(), swap_encoder_registry) + .unwrap(); + + let token_in = weth(); + let token_out = dai(); + + let swap = Swap { + component: ProtocolComponent { + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from(1000000000000000000u64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + checked_token: token_out, + checked_amount: None, + sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), + receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), + swaps: vec![swap.clone(), swap], + slippage: None, + native_action: None, + }; + + let result = encoder.encode_calldata(vec![solution]); + assert!(result.is_err()); + } + + #[test] + fn test_executor_encoder_grouped_swaps() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCommonChain::Ethereum.into(), swap_encoder_registry) + .unwrap(); + + let eth = eth(); + let usdc = usdc(); + let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); + + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); + let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); + let mut static_attributes_eth_pepe: HashMap = HashMap::new(); + static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); + static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); + + let swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + let swap_eth_pepe = Swap { + component: ProtocolComponent { + id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_eth_pepe, + ..Default::default() + }, + token_in: eth.clone(), + token_out: pepe.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("1000_000000").unwrap(), + checked_token: pepe, + expected_amount: Some(BigUint::from_str("105_152_000000000000000000").unwrap()), + checked_amount: None, + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_usdc_eth, swap_eth_pepe], + ..Default::default() + }; + + let transactions = encoder + .encode_calldata(vec![solution]) + .unwrap(); + let transaction = transactions + .first() + .expect("Expected at least one transaction"); + let hex_protocol_data = encode(&transaction.data); + assert_eq!( + transaction.to, + Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() + ); + assert_eq!( + hex_protocol_data, + String::from(concat!( + // group in token + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // group out token + "6982508145454ce325ddbe47a25d4ec3d2311933", + // zero for one + "00", + // transfer type + "00", + // receiver + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // first pool intermediary token (ETH) + "0000000000000000000000000000000000000000", + // fee + "000bb8", + // tick spacing + "00003c", + // second pool intermediary token (PEPE) + "6982508145454ce325ddbe47a25d4ec3d2311933", + // fee + "0061a8", + // tick spacing + "0001f4" + )) + ); + } + } +} diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index c83639d..20e821e 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -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 { 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, token: Bytes) -> Result Result .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>, 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 { + let mut encoded_action_data: Vec = 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> = 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; diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 43af861..052df36 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -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, } +/// 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, 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 for Chain { - fn from(chain: TychoCoreChain) -> Self { +impl From 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() }, } } } diff --git a/src/encoding/tycho_encoder.rs b/src/encoding/tycho_encoder.rs index 09a9d46..16e0bb0 100644 --- a/src/encoding/tycho_encoder.rs +++ b/src/encoding/tycho_encoder.rs @@ -14,8 +14,7 @@ pub trait TychoEncoder { /// /// # Returns /// * `Result, EncodingError>` - Vector of executable transactions - fn encode_router_calldata( - &self, - solutions: Vec, - ) -> Result, EncodingError>; + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError>; + + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>; }